feat(op): support resource indicators (RFC 8707) at the authorization endpoint - #955
feat(op): support resource indicators (RFC 8707) at the authorization endpoint#955amartya-dev wants to merge 5 commits into
Conversation
… 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
left a comment
There was a problem hiding this comment.
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.
|
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:
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 The device flow needed one exception. Storing the resources requires an extra argument on One thing I found along the way: the authorization path of the new 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 No rush on any of this, and thanks for taking the time given #785. |
Which Problems Are Solved
oidc.AuthRequesthas no field for theresourceparameter of RFC 8707 (Resource Indicators for OAuth 2.0), so the parameter is silently dropped at the authorization endpoint. AStorageimplementation 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.resourceout of the rawhttp.Requestbefore it reachesop.Authorizeand 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.resourcevalues, 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
resourceparameter. 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 endpointoidc.AuthRequestgainsResource []stringwith theresourceschema tag, so repeatedresourceparameters are decoded at/authorizeand reachStorage.CreateAuthRequeston the*oidc.AuthRequestthat is already passed to it. No interface change is required, so this is fully backwards compatible.op.ValidateResourceIndicatorsvalidates 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 theinvalid_targeterror code the RFC specifies.CopyRequestObjectToAuthRequestcopiesResourcefrom a Request Object, consistent with how every other authorization parameter is handled there.oidc.DiscoveryConfigurationgainsResourceIndicatorsSupported(resource_indicators_supported,omitempty), driven by a newop.Config.ResourceIndicatorsSupportedfield via theop.ResourceIndicatorsSupported(Configuration)helper. This follows the existingop.Scopes(Configuration)/Config.SupportedScopespattern, so it is an additive field on theConfigstruct rather than a new method on theConfigurationinterface. It defaults tofalseand is omitted from the discovery document.2.
feat(op): support resource indicators (RFC 8707) at the token endpointCovers RFC 8707, section 2.2, which is what lets a client narrow the audience of the token it actually receives.
Resourceis added tooidc.AccessTokenRequest,oidc.RefreshTokenRequestandoidc.ClientCredentialsRequest.op.ValidateTokenRequestResourceschecks the syntax and, in addition, that every requested resource was granted by the original authorization request. A resource that was not granted is rejected withinvalid_target.Because the library cannot know how a resource maps onto an audience, the values are handed to the
Storageimplementation 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
SetCurrentScopesonop.RefreshTokenRequest. An implementation that implements neither keeps its current behaviour exactly.ValidateAuthReqResourcesfrom the first commit is renamed toValidateResourceIndicators, since it is no longer specific to the authorization request.While wiring this up I noticed that the authorization path of the new
ServerAPI (webServer.authorize) mirrorsValidateAuthRequestClientbut 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 flowResourceis added tooidc.DeviceAuthorizationRequestandoidc.DeviceAccessTokenRequest, and both are validated.op.DeviceAuthorizationStatecarries the values and implementsResourceRequestandCurrentResourceSetter, so a device access token request can narrow the granted resources down.DeviceAuthorizationStorage.StoreDeviceAuthorizationwithout breaking every implementation. The optionalop.CanStoreDeviceAuthorizationWithResourcesinterface is used instead when a storage implements it, following the pattern of the other optionalCan...storage interfaces.4.
feat(rp): add WithResourceURLParam optionandfeat(example): bind the token audience to the requested resourcesrp.WithResourceURLParamis aURLParamOpt, so it applies to both the authorization request and the token request. It sets a single value:oauth2.SetAuthURLParamdoesurl.Values.Set, so a repeated URL parameter cannot be expressed through anoauth2.AuthCodeOption. That limitation is stated on the option rather than silently dropping extra values.Tests are added in the existing table-driven style:
TestValidateResourceIndicators,TestValidateTokenRequestResources,TestParseTokenRequestResource,TestDeviceAuthorizationResources,TestCopyRequestObjectToAuthRequest,Test_ResourceIndicatorsSupported,TestWithResourceURLParam,TestAuthRequestResources,TestRefreshTokenRequestResources, plus new cases inTestParseAuthorizeRequest,TestValidateAuthRequest,TestParseDeviceCodeRequestandTestDiscover.Additional Changes
resourcevalue is now rejected withinvalid_targetwhere 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 behindConfig.ResourceIndicatorsSupported, I am happy to change it.resource_indicators_supportedis 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.TestRoutes/…andTestServerRoutes/…, compareexpires_inagainst299and fail on a fast machine when the value is still300. They fail the same way on an unmodifiedmain, so they are untouched here.Deliberately Out of Scope
rp.WithResourceURLParam, which needs a change towithURLParamthat goes aroundoauth2.AuthCodeOption.Resourcefield and is left alone.Additional Context