Skip to content

feat(op): support resource indicators (RFC 8707) at the authorization endpoint - #955

Open
amartya-dev wants to merge 5 commits into
zitadel:mainfrom
amartya-dev:feat/794-resource-indicators
Open

feat(op): support resource indicators (RFC 8707) at the authorization endpoint#955
amartya-dev wants to merge 5 commits into
zitadel:mainfrom
amartya-dev:feat/794-resource-indicators

Conversation

@amartya-dev

@amartya-dev amartya-dev commented Aug 23, 2026

Copy link
Copy Markdown

Which Problems Are Solved

  • oidc.AuthRequest has no field for the resource parameter of RFC 8707 (Resource Indicators for OAuth 2.0), so the parameter is silently dropped at the authorization endpoint. A Storage implementation never learns which resource a token was requested for, and therefore cannot bind the audience of the issued token to it. The same is true at the token endpoint and in the device authorization flow.
  • The only workaround today is to read resource out of the raw http.Request before it reaches op.Authorize and thread it into the audience by hand. That is easy to get subtly wrong — the failure mode is a token with a wildcard or default audience, which defeats the point of asking for a resource in the first place.
  • Nothing validates the syntax of resource values, and there is no way for an OP to advertise the capability in its discovery document.

The motivation is audience binding for MCP servers. MCP revision 2026-07-28 makes every MCP server an OAuth 2.1 resource server: the server MUST validate that a token's audience is itself, and the client MUST send the RFC 8707 resource parameter. Resource indicators are the mechanism that audience binding depends on.

How the Problems Are Solved

The PR is split into four commits, each of which builds and tests on its own, so they can be read (or split into separate PRs) one at a time.

1. feat(op): support resource indicators (RFC 8707) at the authorization endpoint

  • oidc.AuthRequest gains Resource []string with the resource schema tag, so repeated resource parameters are decoded at /authorize and reach Storage.CreateAuthRequest on the *oidc.AuthRequest that is already passed to it. No interface change is required, so this is fully backwards compatible.
  • op.ValidateResourceIndicators validates the values per RFC 8707, section 2: each value must be an absolute URI and must not include a fragment component. A query component is explicitly allowed, as the RFC permits. Invalid values are rejected with the invalid_target error code the RFC specifies.
  • CopyRequestObjectToAuthRequest copies Resource from a Request Object, consistent with how every other authorization parameter is handled there.
  • oidc.DiscoveryConfiguration gains ResourceIndicatorsSupported (resource_indicators_supported, omitempty), driven by a new op.Config.ResourceIndicatorsSupported field via the op.ResourceIndicatorsSupported(Configuration) helper. This follows the existing op.Scopes(Configuration) / Config.SupportedScopes pattern, so it is an additive field on the Config struct rather than a new method on the Configuration interface. It defaults to false and is omitted from the discovery document.

2. feat(op): support resource indicators (RFC 8707) at the token endpoint

Covers RFC 8707, section 2.2, which is what lets a client narrow the audience of the token it actually receives.

  • Resource is added to oidc.AccessTokenRequest, oidc.RefreshTokenRequest and oidc.ClientCredentialsRequest.

  • op.ValidateTokenRequestResources checks the syntax and, in addition, that every requested resource was granted by the original authorization request. A resource that was not granted is rejected with invalid_target.

  • Because the library cannot know how a resource maps onto an audience, the values are handed to the Storage implementation through two optional interfaces on the request types it already returns:

    • op.ResourceRequest (GetResource() []string) reports the granted resources;
    • op.CurrentResourceSetter (SetCurrentResources([]string)) receives the narrowed set before the tokens are created.

    This mirrors the existing SetCurrentScopes on op.RefreshTokenRequest. An implementation that implements neither keeps its current behaviour exactly.

  • ValidateAuthReqResources from the first commit is renamed to ValidateResourceIndicators, since it is no longer specific to the authorization request.

  • While wiring this up I noticed that the authorization path of the new Server API (webServer.authorize) mirrors ValidateAuthRequestClient but never called the resource validation, so the first commit only validated on the legacy path. That is fixed here.

3. feat(op): support resource indicators (RFC 8707) in the device flow

  • Resource is added to oidc.DeviceAuthorizationRequest and oidc.DeviceAccessTokenRequest, and both are validated.
  • op.DeviceAuthorizationState carries the values and implements ResourceRequest and CurrentResourceSetter, so a device access token request can narrow the granted resources down.
  • Storing the resources needs an extra argument, which cannot be added to DeviceAuthorizationStorage.StoreDeviceAuthorization without breaking every implementation. The optional op.CanStoreDeviceAuthorizationWithResources interface is used instead when a storage implements it, following the pattern of the other optional Can... storage interfaces.

4. feat(rp): add WithResourceURLParam option and feat(example): bind the token audience to the requested resources

  • rp.WithResourceURLParam is a URLParamOpt, so it applies to both the authorization request and the token request. It sets a single value: oauth2.SetAuthURLParam does url.Values.Set, so a repeated URL parameter cannot be expressed through an oauth2.AuthCodeOption. That limitation is stated on the option rather than silently dropping extra values.
  • The example storage now adds the requested resources to the audience of the issued tokens, and carries them on the stored access and refresh tokens so that a refresh request can narrow them again. Without this the example did not show what the feature is actually for.

Tests are added in the existing table-driven style: TestValidateResourceIndicators, TestValidateTokenRequestResources, TestParseTokenRequestResource, TestDeviceAuthorizationResources, TestCopyRequestObjectToAuthRequest, Test_ResourceIndicatorsSupported, TestWithResourceURLParam, TestAuthRequestResources, TestRefreshTokenRequestResources, plus new cases in TestParseAuthorizeRequest, TestValidateAuthRequest, TestParseDeviceCodeRequest and TestDiscover.

Additional Changes

  • Behaviour note: a request that carries a syntactically invalid resource value is now rejected with invalid_target where it was previously ignored. This is what the RFC requires, but it is a change in behaviour for a provider whose clients currently send malformed values. If you would rather have this gated behind Config.ResourceIndicatorsSupported, I am happy to change it.
  • resource_indicators_supported is not an IANA-registered authorization server metadata parameter — RFC 8707 does not define one. It is the name used by convention to advertise the capability, and this is noted in the field's doc comment. Happy to drop it if you would prefer not to emit an unregistered parameter.
  • Two pre-existing tests, TestRoutes/… and TestServerRoutes/…, compare expires_in against 299 and fail on a fast machine when the value is still 300. They fail the same way on an unmodified main, so they are untouched here.

Deliberately Out of Scope

  • A repeated-value variant of rp.WithResourceURLParam, which needs a change to withURLParam that goes around oauth2.AuthCodeOption.
  • Token exchange already has a Resource field and is left alone.

Additional Context

… endpoint

The `resource` parameter was silently dropped at the authorization
endpoint, so a Storage implementation had no way to learn which resource
a token was requested for and could not bind the token audience to it.

Add `Resource` to `oidc.AuthRequest` so the parameter is parsed and
handed to `Storage.CreateAuthRequest`, validate the values per RFC 8707
section 2 (absolute URI, no fragment) and reject invalid ones with
`invalid_target`, copy the values from a Request Object like the other
authorization parameters, and let an OP advertise
`resource_indicators_supported` in its discovery document through the
new `Config.ResourceIndicatorsSupported` option.
@wim07101993
wim07101993 self-requested a review August 28, 2026 14:20

@wim07101993 wim07101993 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thank you for the contribution and taking notice of #785.

The changes look good so far. However, I think we need to have more than only accepting the resource indicators in the auth request. Right now the feature is not yet finished, so I won't be able to merge it. Maybe you can make this a stacked pr with the other changes on top?

Once the feature is complete, I am happy to review again.

The `resource` parameter was only accepted at the authorization
endpoint, so a client could not narrow the audience of the token it
receives when it exchanges a code or refreshes a token, which is what
RFC 8707 section 2.2 defines the parameter for.

Add `Resource` to `oidc.AccessTokenRequest`, `oidc.RefreshTokenRequest`
and `oidc.ClientCredentialsRequest`, and validate the values on the
token endpoint: the syntax as at the authorization endpoint, and, in
addition, that every requested resource was granted by the original
authorization request. A resource that was not granted is rejected with
`invalid_target`.

Since the library cannot know how a resource maps onto an audience, the
requested values are handed to the Storage implementation through two
optional interfaces on the request types it returns: `ResourceRequest`
reports the granted resources and `CurrentResourceSetter` receives the
narrowed ones before the tokens are created, mirroring the existing
`SetCurrentScopes` of `RefreshTokenRequest`. Implementations that do not
implement them keep their current behaviour.

`ValidateAuthReqResources` is renamed to `ValidateResourceIndicators`,
as it is no longer specific to the authorization request, and is now
also called on the authorization path of the new `Server` API, which
was missed.
The device authorization endpoint dropped the `resource` parameter, so
a device could not ask for a token bound to the resource server it
intends to call.

Add `Resource` to `oidc.DeviceAuthorizationRequest` and
`oidc.DeviceAccessTokenRequest` and validate both, and carry the values
on `op.DeviceAuthorizationState`, which now implements the
`ResourceRequest` and `CurrentResourceSetter` interfaces so a device
access token request can narrow the granted resources down.

Storing the resources requires an additional argument, which cannot be
added to `DeviceAuthorizationStorage.StoreDeviceAuthorization` without
breaking every implementation. The optional
`CanStoreDeviceAuthorizationWithResources` interface is used instead
when a storage implements it, following the pattern of the other
optional `Can...` storage interfaces. The example storage implements it.
`rp.WithURLParam` could already set a `resource` parameter, but callers
had to spell the parameter name themselves. Add a dedicated option, as
for the other well-known parameters.

As a `URLParamOpt` it applies to both the authorization request and the
token request, which is what RFC 8707 needs to bind the audience of the
issued token. Only a single value can be set, because the underlying
oauth2 package cannot express a repeated URL parameter; this is stated
on the option.
The example storage always used the client_id as the audience, so the
resource indicators it now receives had no visible effect and the
example did not show what the feature is for.

Add the requested resources to the audience of the issued tokens and
carry them on the stored access and refresh tokens, so a refresh token
request can narrow them down again. The client_id is kept in the
audience because an ID token must be addressed to the client it was
issued for; a real implementation would check the requested resources
against a policy of the client first and would likely restrict the
audience of the access token to the resources alone, which is noted
where the audience is built.
@amartya-dev

Copy link
Copy Markdown
Author

Thanks for the review — that is fair, the authorization endpoint on its own does not give anyone a usable feature. I have pushed the rest of it on top, as four commits that each build and test on their own:

  1. f6420eatoken endpoint (RFC 8707 §2.2): resource on AccessTokenRequest, RefreshTokenRequest and ClientCredentialsRequest, validated both for syntax and for having been granted by the original authorization request, otherwise invalid_target.
  2. e854fa4device flow: resource on the device authorization and device access token requests, carried on op.DeviceAuthorizationState.
  3. c360872rp.WithResourceURLParam.
  4. 5905998example storage, so the requested resources actually reach the token audience and the example shows what the feature is for.

Two design points worth your attention, since they are the parts I would most like a second opinion on:

Nothing breaks existing implementations. The library cannot know how a resource maps onto an audience, so the requested values reach Storage through two optional interfaces on the request types it already returns: op.ResourceRequest (GetResource) reports what was granted, and op.CurrentResourceSetter (SetCurrentResources) receives the narrowed set before the tokens are created. That mirrors the existing SetCurrentScopes on op.RefreshTokenRequest. An implementation that implements neither behaves exactly as it does today.

The device flow needed one exception. Storing the resources requires an extra argument on DeviceAuthorizationStorage.StoreDeviceAuthorization, which cannot be added without breaking every implementation, so there is an optional op.CanStoreDeviceAuthorizationWithResources that is used when a storage implements it — following the existing Can... storage interfaces. If you would rather see this shape differently, it is an easy change.

One thing I found along the way: the authorization path of the new Server API (webServer.authorize) mirrors ValidateAuthRequestClient but never called the resource validation, so my first commit only validated on the legacy path. That is fixed in f6420ea.

On "stacked PR" — I was not sure whether you meant these commits on top of this one, or separate PRs. I have gone with commits on top, since separate PRs from a fork cannot actually be based on each other (GitHub wants the base branch in this repo), so they would each show a cumulative diff against main anyway. If you would still rather review them as four separate PRs, say the word and I will split them — the commits are already clean cut for it.

No rush on any of this, and thanks for taking the time given #785.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support of RFC-8707

2 participants