Skip to content

feat(auth): support RFC 8707 resource indicators on the PKCE provider - #120

Merged
scottlovegrove merged 2 commits into
mainfrom
feat/pkce-resource-indicator
Aug 27, 2026
Merged

feat(auth): support RFC 8707 resource indicators on the PKCE provider#120
scottlovegrove merged 2 commits into
mainfrom
feat/pkce-resource-indicator

Conversation

@scottlovegrove

Copy link
Copy Markdown
Collaborator

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 it doesn't need or reimplement AuthProvider outright.

This adds resource to PkceProviderOptions, taking the same literal-or-resolver form as authorizeUrl / tokenUrl / clientId. It is applied to the authorize URL and to both the authorization_code and refresh_token token requests.

The refresh leg is the part with no workaround today. tokenRequestParams could already inject a resource into the code exchange, but nothing could reach the refresh grant — refreshToken built its TokenEndpointRequestOptions with only signal and customFetch — 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:

  • resolveResource moves from dcr.ts into the shared oauth.ts helpers, now that both providers use it. No behaviour change on the DCR side.
  • On the token request the resource is set before the caller's tokenRequestParams extras, so a consumer already injecting one by hand keeps winning. That preserves the precedence that held before resource became 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 carry resource=https://automations.todoist.com so 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_id is, so the document URL passes straight through. The alternative was createDcrProvider with a stub loadClient that short-circuits registration and returns a hardcoded client, which works but reads as a lie and drags in the oauth4webapi peer dep for login. With this change it is plain createPkceProvider, 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-document client_id), lazy resolution from handshake/flags, resource on the refresh grant, tokenRequestParams overriding it, and no stray resource parameter anywhere when the option is unset. Full suite green: 540 passing.

`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
@scottlovegrove scottlovegrove self-assigned this Aug 27, 2026

@doistbot doistbot 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.

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.resource override can break RFC 8707 compliance on the code exchange: if the configured resource is A but the override returns B, the authorization request carries A while the token request carries B, which a compliant server must reject. Consider making the configured resource win, or deriving one effective resource for both requests.
  • tokenRequestParams is built after resource resolution finishes, but its inputs (handshake, flags) are already available — starting it in parallel would make exchange latency max(tokenUrl, resource, extras) instead of max(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)
  • P3 src/auth/providers/pkce.ts:226: The inline additionalParameters: { resource } + [oauth.customFetch] construction here duplicates exactly what dcr.ts's tokenRequestOptions(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 moved resolveResource into oauth.ts for the same sharing reason. Consider extracting tokenRequestOptions (or an equivalent refreshTokenRequestOptions) into oauth.ts and reusing it from both providers instead of re-rolling it inline in refreshToken.

Share FeedbackReview Logs

Comment thread src/auth/providers/pkce.ts
…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
scottlovegrove requested review from a team and craigcarlyle and removed request for a team August 27, 2026 13:42
@scottlovegrove scottlovegrove added the 👀 Show PR PR must be reviewed before or after merging label Aug 27, 2026
@scottlovegrove
scottlovegrove merged commit deea470 into main Aug 27, 2026
5 checks passed
@scottlovegrove
scottlovegrove deleted the feat/pkce-resource-indicator branch August 27, 2026 13:44
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))
@doist-release-bot

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.3.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@craigcarlyle

Copy link
Copy Markdown

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

Labels

released 👀 Show PR PR must be reviewed before or after merging

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants