Resource indicators (RFC 8707)
A grant can reach several APIs, and without an indicator every access token it yields is addressed to all of them — so
any one of those APIs, holding it, could replay it against the rest. resource
(RFC 8707) asks for a token addressed to one. The recorded flow
shows a client asking for a token for one API, then another, then one it may not have. The code on this page is included
from samples/Protocols.Resources,
which a test asks for tokens with and without an indicator.
At the token endpoint
New in 0.77.0. A token request naming resource is answered with an access token whose aud is that API alone, and
whose scope is only the scopes it exposes. An API can refuse to be one of several audiences at all:
// In a token's audience only when a resource indicator names it, and then alone.
new ApiResource("https://ledger.example.com")
{
Scopes = { "ledger.read" },
RequireResourceIndicator = true,
},
Without resource |
With resource |
|
|---|---|---|
| audience | every API the grant reaches, except those with RequireResourceIndicator | that API alone |
| API scopes in the token | all granted | only those the named API exposes, and offline_access — not the identity scopes |
| refresh token, identity token | the whole grant | still the whole grant — narrowing one access token does not narrow what the client was given |
Measured from Duende 8.0.1 and matched: one indicator at the token endpoint, an absolute URI with no fragment, at
most InputLengthRestrictions.ResourceIndicator (512) characters, and invalid_target for anything else — including an
indicator for an API the grant's scopes never reached, because an indicator narrows and never widens. The device flow
refuses one rather than ignoring it, as Duende does.
Switching a resource off, since 0.93.0. Enabled = false on an ApiResource, ApiScope or IdentityResource means
what Duende means by it: a disabled scope is refused as invalid_scope, a disabled API is in no token's audience —
whether or not an indicator names it — discovery advertises neither, and userinfo asks the profile service for no claim a
disabled identity resource names. Before it only introspection and an indicator naming the API read the flag.
At the authorization endpoint
New in 0.78.0, several since 0.94.0. The grant itself can name the APIs it is for: name them in the authorization
request — as many resource parameters as there are APIs — and the code and the refresh token it leads to remember them.
A token is then addressed to one of them at a time, by naming it.
GET /connect/authorize?client_id=resource-web&response_type=code&scope=openid%20ledger.read%20offline_access
&resource=https%3A%2F%2Fledger.demo.example.com&resource=https%3A%2F%2Forders.demo.example.com
&redirect_uri=…&code_challenge=…&code_challenge_method=S256
| Result | |
|---|---|
| any indicator too long, malformed, or naming no enabled API the requested scopes reach | invalid_target, by redirect |
| a later token request naming an API the grant did not name | invalid_target |
| a later token request naming one it did | a token for that API alone |
| a later token request naming none | a token for every API the scopes reach, except those with RequireResourceIndicator |
Measured from Duende 8.0.1's authorize and token request validators, and matched — the pushed-request endpoint, a request
object (an array of resource values) and the return URL carry every indicator, joined with a space as scope is, since
an absolute URI cannot contain one.
Corrected in 0.94.0. From 0.78.0 to 0.93.0 this server also refused, as
invalid_scope, a requested API scope the named API did not expose, and gave a token request naming no indicator the grant's one indicator. Both were documented as measured from Duende, and neither is Duende's: its resource validator finds an API scope by name alone — theinvalid_scopecame from reading a log line as a rule — and an exchange naming nothing is filtered as above. An indicator decides which APIs a token may be addressed to, not which scopes the grant may hold.
On a backchannel request
New in 0.79.0, several since 0.94.0. A CIBA request is the backchannel counterpart of an authorization
request, so it takes resource with the same rules — from the form or from inside a signed request — and the grant
remembers every API it names: a poll, a push and every refresh after them may name one of those, and a poll naming
another is invalid_target. A user who unticks every scope a named API exposes leaves a grant that no longer reaches it;
a poll naming that API is then invalid_target, because the API the client asked for is the one the user did not, in the
end, allow.
The rules live in one place, ResourceIndicatorValidation, called by the authorization, backchannel and token endpoints
alike.