A RAR entry is a product
Last month I wrote a few posts about putting a policy decision point behind an OAuth token endpoint. This one’s about a mapping problem I’d gotten wrong in that work, which got pointed out to me, and where the fix turned out to be more interesting than the bug was.
Rich Authorization Requests (RFC 9396)
gave OAuth a way for a client to ask for something more expressive than a
space-delimited scope string: a JSON array called authorization_details, where
each entry names a type and then, optionally, the actions it wants, the
locations it wants them at, and the kinds of data it wants to touch. It’s the
richest statement of intent a client can put in a token request. And RFC 9396
requires the authorization server to process it, requires it to obtain consent,
and then says that how the AS decides what to actually grant is domain-specific
and out of scope.
Which is the same silence those earlier posts were about. RFC 6749 permits an AS to issue narrower scopes than requested and never says on what basis. UMA defines the moment and declines to standardize it. RAR does it again, and does it for the richest input of the three.
We tried this in 2024 and thought about it wrong
Back in 2024, some of us in the AuthZEN working group tried to connect RAR and
AuthZEN. The draft was
draft-brossard-oauth-rar-authzen,
and honestly, we kind of thought about it wrong. Our approach was to offer an
alternative way of expressing a rich authorization request that’s identical to an
AuthZEN payload, which essentially implies that the information model in RFC 9396
is wrong. That went over like a lead balloon at IETF 120, which in hindsight
was… predictable.
This attempt starts from the other end. It takes the RAR information model as
RFC 9396 defines it and maps that model onto the AuthZEN subject, action, resource
and context. Nothing new on the wire, no new authorization_details type, no
client involvement at all: the client sends ordinary RAR, and the AS derives the
evaluation from what it already received.
The gap that was still there
Even so, the framework draft had the response half and not the request half. It
defined an authorization_details key the PDP could return to narrow a grant,
and it said nothing at all about getting the requested entries to the PDP.
Karl McGuinness filed issue 9
on it.
An entry is a product, not a permission
Turns out RFC 9396 already tells you where the seams are. From section 2.2:
When different common data fields are used in combination, the permissions the client requests are the product of all the values. The object represents a request for all
actionsvalues listed within the object to be used at alllocationsvalues listed within the object for alldatatypesvalues listed within the object.
An entry with two locations and two datatypes isn’t one permission. It’s four. And Figure 6 of that same document is the escape hatch for a client that doesn’t want the whole product, which sends several entries instead. So asking the PDP one question per cell is asking exactly the questions the entry poses, and asking any fewer answers something coarser than what the client said.
The decomposition falls out of that:
| RAR member | AuthZEN |
|---|---|
type | resource.type |
each locations value | resource.id |
each actions value | action.name, leading segments |
each datatypes value | action.name, final segment |
read on contacts becomes the action name read:contacts. Everything goes into
a batch under execute_all, alongside the
gate tuples that authorize the token itself.
Absent members get substituted rather than skipped. An entry with no actions uses
a reserved name. An entry with no locations fans across the request’s own targets,
which continues the audience restriction that locations performs in the first
place, and matches RFC 9396’s own advice about filtering authorization details to
the audience when you mint a JWT.
What that buys, and it’s more than I expected
The obvious win is that a denied cell narrows an entry instead of failing it, which is the thing everybody actually uses RAR for and the thing the 2024 draft never had anything to say about.
The less obvious win is that the surviving cells need not be a rectangle. Take an entry asking to read contacts and photos at two locations, and a PDP that permits everything except photos in the EU:
contacts | photos | |
|---|---|---|
example.com/customers | permit | permit |
eu.example.com/customers | permit | deny |
Three cells survive and three cells aren’t a product, so there’s no single entry that describes them. The AS returns two entries of the same type instead, which is precisely the shape Figure 6 defines. The rule that matters is the one that says you may not widen to the smallest single entry containing the survivors, because that entry contains a cell the PDP denied.
And the structural check on the response becomes derivable rather than asserted.
An entry whose locations, actions and datatypes are subsets of a request
entry’s is exactly an entry reassembled from cells that were permitted. The rule
was in the draft before the request side existed, where it was a rule I’d simply
declared. Now it’s a consequence.
Where it stops
Two honest limits.
The decomposition doesn’t reach privileges or identifier, because section
2.2’s product sentence names actions, locations and datatypes and stays silent on
the first while the second is a scalar. More importantly it doesn’t reach a type
that carries its multiplicity inside type-specific members rather than in the
common data fields, which is to say a consent object that enumerates individual
permissions. That’s the prevailing shape in deployed open banking profiles, so it
isn’t a corner case, and such an entry yields one evaluation per location and no
more. The PDP can still narrow it by policy through the response key, subject to
the same structural check, but it’s narrowing something it wasn’t shown.
The other limit is the bill. One token request now becomes many evaluations, and the client controls the size of every entry. The product is the legitimate meaning of a RAR entry rather than an abuse of it, but a feature is only a feature until it’s used to excess, and an authenticated but unprivileged client that sends a large enough entry imposes real work on a PDP that other tenants are also using. Two controls, and they do different jobs: strip out what the AS was never going to grant before you form the batch, and cap what is left. Neither substitutes for the other, and neither substitutes for watching fan-out per client over time.
The takeaway
I spent a while looking for the right way to represent a RAR entry to a policy engine and the answer was that RFC 9396 had already cut it up for me. The product sentence in section 2.2 isn’t incidental prose. It’s telling you how many permissions an entry actually contains, and once you read it that way the mapping onto a subject, an action, a resource and some context isn’t a design decision at all, it’s bookkeeping.
The part I wouldn’t have predicted is that decomposing the request is what made the response rules honest. I’d written the narrowing constraints first and justified them by argument. Sending the PDP the actual cells turned every one of them into something you can derive.