Skip to content

feat(auth): add resource indicator, refresh, and client caching to DCR provider#60

Merged
scottlovegrove merged 3 commits into
mainfrom
feat/dcr-resource-refresh
Jun 5, 2026
Merged

feat(auth): add resource indicator, refresh, and client caching to DCR provider#60
scottlovegrove merged 3 commits into
mainfrom
feat/dcr-resource-refresh

Conversation

@scottlovegrove
Copy link
Copy Markdown
Collaborator

Why

createDcrProvider covered RFC 7591 dynamic client registration + PKCE code exchange, but a consumer that needs any of the following had to abandon the factory and hand-roll a full AuthProvider (raw fetch DCR + PKCE + token exchange + refresh). This was discovered while reviewing comms-cli, whose auth-provider.ts reimplements ~500 lines of OAuth that this factory already owns, purely to add three missing capabilities:

  1. RFC 8707 resource indicator — when the authorization server (e.g. Todoist) and the protected resource (e.g. Comms) differ, the token's audience must be pinned via resource on the authorize URL and token requests. The factory had no hook for it.
  2. Refresh — the DCR provider didn't implement refreshToken at all (only createPkceProvider did), so DCR consumers couldn't use cli-core's silent-refresh path.
  3. Client cachingprepare() re-registered on every login; no way to reuse an issued client_id.

What

  • resource?: OAuthLazyString — threaded into the authorize URL (new optional additionalParameters on buildPkceAuthorizeUrl) and the authorization_code + refresh_token token request bodies (oauth4webapi additionalParameters).
  • refreshToken on the DCR provider — honours the handshake's client auth (public None / client_secret_post / RFC-3986 Basic), the resource indicator, and a 10s timeout; maps invalid_grant → AUTH_REFRESH_EXPIRED, else AUTH_REFRESH_TRANSIENT.
  • loadClient / saveClient hooks — reuse a registered client across logins. A cache hit skips both the registration round-trip and the oauth4webapi load. cli-core stays storage-agnostic; the consumer owns persistence.

Internal cleanup

  • Extracted the refresh-error mapping into oauth.ts as mapRefreshError and reused it from createPkceProvider (−38 lines of duplicated mapping there).
  • Extracted selectClientAuth / tokenRequestOptions / clientHandshake helpers in dcr.ts, shared by exchangeCode and the new refreshToken.

Tests

9 new dcr.test.ts cases: resource on authorize URL + token body, refresh grant (+ resource), confidential-client refresh auth, invalid_grant → AUTH_REFRESH_EXPIRED, missing-clientId → AUTH_REFRESH_UNAVAILABLE, loadClient cache hit (no registration POST), and saveClient on a cache miss. Full suite green, type-check + lint + format clean.

🤖 Generated with Claude Code

…R provider

Extend createDcrProvider so RFC 7591 DCR consumers can cover three cases the
factory previously forced them to hand-roll a bespoke AuthProvider for:

- RFC 8707 resource indicator: new `resource` option, threaded into the
  authorize URL and the authorization_code + refresh_token token requests so
  the AS issues a token whose audience targets the protected resource.
- refreshToken: the DCR provider now implements it (previously only
  createPkceProvider did), honouring the handshake client auth (public None /
  client_secret_post / RFC-3986 Basic), the resource indicator, and a 10s
  timeout, mapping invalid_grant -> AUTH_REFRESH_EXPIRED else
  AUTH_REFRESH_TRANSIENT.
- client caching: optional loadClient/saveClient hooks let a consumer reuse a
  registered client across logins (cache hit skips the registration round-trip
  and the oauth4webapi load); cli-core stays storage-agnostic.

Factor the shared refresh-error mapping into oauth.ts (mapRefreshError) and
reuse it from createPkceProvider, and add an optional additionalParameters
field to buildPkceAuthorizeUrl.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@scottlovegrove scottlovegrove self-assigned this Jun 5, 2026
@scottlovegrove scottlovegrove requested a review from amix June 5, 2026 12:37
Copy link
Copy Markdown
Member

@doistbot doistbot left a comment

Choose a reason for hiding this comment

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

Thanks Scott for expanding the DCR provider with resource indicators, refresh capabilities, and client caching 😇.

Few things worth tightening:

  • Apply additionalParameters before standard PKCE parameters so standard values aren't accidentally overwritten.
  • Ensure loadClient validates the returned cache shape, and include the redirect_uri in DcrRegisteredClient so cached clients don't use mismatched callbacks on subsequent logins.
  • Provide a way for refreshToken to reload the cached client itself, since the standard silent-refresh path doesn't persist the DCR handshake.
  • Update the README.md to reflect the new API and remove the outdated caching claim, and ensure tests cover async cache hooks and distinct auth-method precedence.

I also included a few optional follow-up notes in the details below.

Optional follow-up note (1)
  • [P3] src/auth/providers/dcr.ts:349: tokenUrl and resource are resolved serially here. Both are OAuthLazyStrings, so if a consumer supplies async resolvers, every silent refresh now waits for them back-to-back. Resolve them with Promise.all (and likewise in the new authorize/exchangeCode resource lookups) to avoid adding avoidable latency on this path.

Share FeedbackReview Logs

Comment thread src/auth/providers/oauth.ts Outdated
Comment thread src/auth/providers/dcr.ts
Comment thread src/auth/providers/dcr.ts
Comment thread src/auth/providers/dcr.ts Outdated
Comment thread src/auth/providers/dcr.test.ts Outdated
Comment thread src/auth/providers/dcr.test.ts Outdated
Comment thread src/auth/index.ts
- buildPkceAuthorizeUrl: apply additionalParameters before the standard PKCE
  params so the latter always win (the loop previously ran after, enabling the
  clobbering the doc claimed to prevent).
- Validate the loadClient cache shape (string clientId, supported
  tokenEndpointAuthMethod) so a cached client behaves like a freshly registered
  one instead of failing later or silently using the wrong auth method.
- Resolve tokenUrl/authorizeUrl and the resource indicator concurrently
  (Promise.all) on the authorize, exchange, and refresh paths.
- Document the loadClient redirect-URI binding (registration is bound to its
  redirect_uris; cache must be keyed on PrepareInput.redirectUri) and the DCR
  refresh handshake contract (clientId must be supplied on the refresh
  handshake, reconstructed from persisted account metadata).
- Tests: make the refresh auth-method test prove handshake-over-config
  precedence (differing values), exercise async loadClient/saveClient, and
  cover malformed-cache rejection.
- README: document resource indicators, refresh, and client caching; drop the
  stale "does not cache" claim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Member

@amix amix left a comment

Choose a reason for hiding this comment

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

Looked through the code and reads well.

I think it’s critical that we test the refresh paths using comms-cli / ⁠todoist-cli to ensure everything works properly. There’s a lot of complexity in this flow, and plenty of places where things could break ... either in the implementation or on the backend.

@scottlovegrove
Copy link
Copy Markdown
Collaborator Author

I think it’s critical that we test the refresh paths using comms-cli

Yup, just linking this PR to Comms CLI locally now to try it out.

Add an optional `scope` to `ExchangeResult` carrying the raw `scope` from the
token response (RFC 6749 §5.1). Populate it from the authorization_code and
refresh_token grants in both createPkceProvider and createDcrProvider (and from
postTokenEndpoint). Lets a provider's validateToken record the
server-authoritative granted scope instead of re-deriving it from the request —
important when the server narrows scope relative to what was asked for.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@scottlovegrove scottlovegrove merged commit 0db0cf3 into main Jun 5, 2026
4 checks passed
@scottlovegrove scottlovegrove deleted the feat/dcr-resource-refresh branch June 5, 2026 13:19
doist-release-bot Bot added a commit that referenced this pull request Jun 5, 2026
## [0.25.0](v0.24.0...v0.25.0) (2026-06-05)

### Features

* **auth:** add resource indicator, refresh, and client caching to DCR provider ([#60](#60)) ([0db0cf3](0db0cf3))
@doist-release-bot
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 0.25.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants