Skip to content

fix(server)!: confine resource-less requests to AllowedResources; resources at consent - #699

Merged
osanderson merged 5 commits into
mainfrom
fix/resource-chain-review
Oct 25, 2026
Merged

osanderson merged 5 commits into
mainfrom
fix/resource-chain-review

Conversation

@osanderson

@osanderson osanderson commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

RFC 8707 resource-indicator fixes around AllowedResources, consent and automatic registration.

Breaking (fix(server)!): a request naming no resource is confined to the client's AllowedResources.
A request that named no resource used to get an access token addressed to the issuer, and that token was never checked against AllowedResources. Every resource server that accepts the issuer as an audience took it: one migrating with serverresource.Options.AcceptIssuerAudience or with the issuer in AdditionalAudiences, and the UserInfo endpoint. So a client refused resource=B got a token B accepts by leaving resource out. Now, when resource indicators are enabled and the client has AllowedResources:

  • One entry: a request naming no resource is for that resource. The grant records it, and it is checked against Config.Resources/ResourcePolicy like a named resource.
  • Several entries: a request naming none is refused with invalid_target ("a resource is required").

This applies to PAR (plain and request object), CIBA, client_credentials and the embedder APIs IssueAccessToken/IssueRefreshToken. It also covers a grant made without resources before the client was confined, at its next code exchange, refresh or CIBA token request. The default lives in checkRequestedResources, which all those paths, including the token-time re-check in narrowedAudience, already call. An omitted resource at the token endpoint still means the whole set the grant holds. Clients without AllowedResources, and servers with resource indicators disabled, behave as before.

This also closes the embedder gap: a refresh token issued through IssueRefreshToken with fewer Resources can no longer widen later tokens to the issuer audience for a confined client. New accessor: storage.RegisteredClient.AllowedResources().

Additive (feat(server)):

  • InteractionRequest.Resources and BackchannelInteractionRequest.Resources: the resources the tokens would be for, including the default above. They are cloned, carried by MarshalText/ParseInteractionRequest, and returned by LookupBackchannelInteraction.
  • GrantedAuthorization.Resources: narrows the request's resources to the subset the user approved, at CompleteAuthorization and CompleteBackchannelAuthentication. Nil grants all of them, which is the old behaviour. A resource that wasn't requested gets invalid_request, the same check used for scopes.
  • AutomaticRegistrationConfig.AllowedResources in both federation and server: an operator-level confinement applied to every client registered through OpenID Federation. It is never read from RP metadata, and it is validated like Config.Resources (absolute URI, no fragment, no repeats).

Docs: updated the doc comments for AllowedResources, Config.Resources, IssueAccessTokenRequest/IssueRefreshTokenRequest.Resources and AcceptIssuerAudience, and GETTING_STARTED's RFC 8707 section. Its old statement that consent can't see resources was removed. UPGRADING.md has a new v0.61.0 section.

Refactors (no behaviour change): completeAuthorize now validates the approved grant in checkApprovedGrant and the login_required conditions in loginRequiredProblem. CIBA hint and binding_message validation moved into validateBackchannelHints and validateBindingMessage. These keep the touched functions within Sonar's cognitive-complexity limit.

Breaking change

A client with AllowedResources no longer gets issuer-audience tokens while resource indicators are enabled:

  • With one entry, its no-resource requests are now for that resource.
  • With several, they fail with invalid_target.
  • A confined client that also calls UserInfo must list the issuer in AllowedResources, and the server must accept the issuer as a resource. Then it must name a resource on every request.

Test plan

  • New end-to-end tests in fapitest/resource_indicators_allowed_test.go, covering the code flow and refresh with JWT and opaque tokens, client credentials, and multi-entry refusal. Each one checks the token is refused by a resource server that accepts the issuer audience and by the issuer's own verifier. The existing client-credentials "no resource" case now expects invalid_target.
  • New server tests: resource_indicators_default_test.go (PAR, the default being re-checked against Config.Resources/the policy, disabled indicators, earlier grants, CIBA, client credentials, embedder access and refresh) and resource_indicators_consent_test.go (interaction resources, narrowing, refusal on CompleteAuthorization, and on CIBA via LookupBackchannelInteraction). Also new federation config tests, a server wiring test and an extended round-trip test.
  • Mutation check: switching the default off makes every new AllowedResources test fail.
  • GOTOOLCHAIN=go1.26.9 go test ./..., go test -race on server, fapitest and federation, go vet ./..., GOOS=linux GOARCH=arm go vet ./..., golangci-lint run ./... (0 issues), and vet and tests for every examples/* module and demokit.

🤖 Generated with Claude Code

osanderson and others added 3 commits October 25, 2026 22:16
…owedResources

A request naming no RFC 8707 resource got an access token addressed to
the issuer, and AllowedResources was never checked against it. Every
resource server accepting the issuer as an audience (one migrating with
AcceptIssuerAudience or the issuer in AdditionalAudiences, and the
UserInfo endpoint) took that token, so a client refused a resource
could reach it by naming none.

For a client with AllowedResources, a request naming no resource now
gets a default: with one entry, it is for that resource (recorded on
the grant and checked against Config.Resources and the ResourcePolicy
as a named one is); with several, it is refused with invalid_target.
The default applies wherever a resource can be named (PAR, CIBA,
client_credentials, IssueAccessToken and IssueRefreshToken) and to a
grant made without resources before the client was confined, at its
next code exchange, refresh or CIBA token request, so neither the
embedder APIs nor an older grant can produce an issuer-audience token
for a confined client. Clients without AllowedResources, and servers
with resource indicators disabled, are unchanged.

RegisteredClient.AllowedResources returns the registered list.

BREAKING CHANGE: a client registered with AllowedResources no longer
gets issuer-audience access tokens when resource indicators are
enabled. With a single entry, a request naming no resource is for that
resource; with several, it fails with invalid_target. Such a client
that calls the UserInfo endpoint must list the issuer identifier in its
AllowedResources (and the server must accept it as a resource), then
name a resource in every request. See UPGRADING.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n clients' resources

InteractionRequest.Resources and BackchannelInteractionRequest.Resources
carry the RFC 8707 resources the tokens would be restricted to (the
client's AllowedResources default included), so where several resource
servers or OpenID4VCI Credential Issuers share the authorization server,
the consent step can show which one a token is for. The encoded
InteractionRequest carries them too. GrantedAuthorization.Resources
narrows them to the subset the resource owner approved; nil grants all
of them, as before, and one that wasn't requested fails the completion
with invalid_request, as an unrequested scope does.

AutomaticRegistrationConfig.AllowedResources (federation and server)
confines every client registered through OpenID Federation to the
operator's resources, the way AllowedScopes sets their scopes: never
read from an RP's own metadata. Each entry must be an absolute URI
without a fragment, none repeated. Empty leaves them unconfined, as
before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

osanderson and others added 2 commits October 25, 2026 22:22
…sage in helpers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… helper

Also covers a non-string CIBA binding_message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@osanderson
osanderson merged commit ec23197 into main Oct 25, 2026
19 checks passed
@osanderson
osanderson deleted the fix/resource-chain-review branch October 25, 2026 21:55
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.

1 participant