Omri Gazitt

A RAR entry is a product

· 6 min read

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 actions values listed within the object to be used at all locations values listed within the object for all datatypes values 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 memberAuthZEN
typeresource.type
each locations valueresource.id
each actions valueaction.name, leading segments
each datatypes valueaction.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:

contactsphotos
example.com/customerspermitpermit
eu.example.com/customerspermitdeny

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.