feat(auth): support RFC 8707 resource indicators on the PKCE provider - #120
Merged
Conversation
`createPkceProvider` had no way to request an audience-targeted token. Only `createDcrProvider` accepted a `resource`, so a public client whose authorization server and API are different origins had to either fake a registration flow or reimplement `AuthProvider` outright. Add `resource` to `PkceProviderOptions`, taking the same literal-or-resolver form as the other endpoints. It is applied to the authorize URL and to both the `authorization_code` and `refresh_token` token requests. The refresh leg matters most: `tokenRequestParams` could already inject a resource into the code exchange, but nothing could reach the refresh grant, so a rotated token came back with the wrong audience. `resolveResource` moves from dcr.ts into the shared oauth.ts helpers, since both providers now use it. On the token request the resource is set before the caller's `tokenRequestParams` extras, preserving the precedence that held when injecting it by hand was the only option. This also makes a client ID metadata document client work without a registration step: the document URL goes in `clientId`, the API it is for goes in `resource`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T1D2UgUZiqouii9bXebRVS
doistbot
reviewed
Aug 27, 2026
doistbot
left a comment
Member
There was a problem hiding this comment.
This PR threads RFC 8707 resource indicators through createPkceProvider — applied to the authorize URL and both token grants — mirroring the existing DCR handling and moving resolveResource into the shared oauth.ts helpers.
Few things worth tightening:
- The
tokenRequestParams.resourceoverride can break RFC 8707 compliance on the code exchange: if the configured resource isAbut the override returnsB, the authorization request carriesAwhile the token request carriesB, which a compliant server must reject. Consider making the configured resource win, or deriving one effective resource for both requests. tokenRequestParamsis built after resource resolution finishes, but its inputs (handshake,flags) are already available — starting it in parallel would make exchange latencymax(tokenUrl, resource, extras)instead ofmax(tokenUrl, resource) + extras, while keeping the existing spread order for override precedence.
I also left one optional follow-up note in the details below.
Optional follow-up note (1)
src/auth/providers/pkce.ts:226: The inline
additionalParameters: { resource }+[oauth.customFetch]construction here duplicates exactly whatdcr.ts'stokenRequestOptions(oauth, fetchImpl, resource, signal)helper already does. Both factories now build the same RFC 8707 / custom-fetch / abort-signal token-request options, and this PR already movedresolveResourceintooauth.tsfor the same sharing reason. Consider extractingtokenRequestOptions(or an equivalentrefreshTokenRequestOptions) intooauth.tsand reusing it from both providers instead of re-rolling it inline inrefreshToken.
…thorize request Applying the configured `resource` before `tokenRequestParams` let a consumer that set both send resource A on the authorize request and resource B on the token request. RFC 8707 §2.2 requires the two to agree, so a conformant server must reject that. Apply it last instead: a configured resource can no longer be displaced, and with the option unset `tokenRequestParams` can still supply one, which is how it had to be done before the option existed. Also start `tokenRequestParams` alongside the endpoint and resource resolvers — its inputs are ready before either resolves, so the exchange now waits on the slowest of the three rather than on their sum. `tokenRequestOptions` moves from dcr.ts into the shared oauth.ts helpers, following `resolveResource`. Both providers were building the same customFetch + resource + signal options object for the refresh grant. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T1D2UgUZiqouii9bXebRVS
scottlovegrove
requested review from
a team and
craigcarlyle
and removed request for
a team
August 27, 2026 13:42
doist-release-bot Bot
added a commit
that referenced
this pull request
Aug 27, 2026
## [1.3.0](v1.2.0...v1.3.0) (2026-08-27) ### Features * **auth:** support RFC 8707 resource indicators on the PKCE provider ([#120](#120)) ([deea470](deea470))
Contributor
|
🎉 This PR is included in version 1.3.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
|
✅ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
createPkceProviderhad no way to request an audience-targeted token. OnlycreateDcrProvideraccepted aresource, so a public client whose authorization server and API are different origins had to either fake a registration flow it doesn't need or reimplementAuthProvideroutright.This adds
resourcetoPkceProviderOptions, taking the same literal-or-resolver form asauthorizeUrl/tokenUrl/clientId. It is applied to the authorize URL and to both theauthorization_codeandrefresh_tokentoken requests.The refresh leg is the part with no workaround today.
tokenRequestParamscould already inject aresourceinto the code exchange, but nothing could reach the refresh grant —refreshTokenbuilt itsTokenEndpointRequestOptionswith onlysignalandcustomFetch— so a rotated token came back with the wrong audience and the CLI would start failing some hours after a successful login.Two smaller things fall out of it:
resolveResourcemoves fromdcr.tsinto the sharedoauth.tshelpers, now that both providers use it. No behaviour change on the DCR side.tokenRequestParamsextras, so a consumer already injecting one by hand keeps winning. That preserves the precedence that held beforeresourcebecame a first-class option.Why now
The Automations CLI needs a client ID metadata document client against Todoist's authorisation server: the document's https URL is the
client_id, there is no registration step, the client is public with mandatory PKCE, and the token has to carryresource=https://automations.todoist.comso it is minted for the Automations API rather than the Todoist one.Everything there except the resource indicator already worked — nothing in cli-core cares what shape a
client_idis, so the document URL passes straight through. The alternative wascreateDcrProviderwith a stubloadClientthat short-circuits registration and returns a hardcoded client, which works but reads as a lie and drags in theoauth4webapipeer dep for login. With this change it is plaincreatePkceProvider, and the README documents that combination as its own quick-start section.Tests
Five new cases on
createPkceProvider: resource on the authorize URL and the code exchange (using a metadata-documentclient_id), lazy resolution from handshake/flags, resource on the refresh grant,tokenRequestParamsoverriding it, and no strayresourceparameter anywhere when the option is unset. Full suite green: 540 passing.