Skip to content

fix(auth): send DCR HTTP Basic credentials raw, not form-url-encoded#43

Merged
scottlovegrove merged 2 commits into
mainfrom
scottl/dcr-raw-basic
May 21, 2026
Merged

fix(auth): send DCR HTTP Basic credentials raw, not form-url-encoded#43
scottlovegrove merged 2 commits into
mainfrom
scottl/dcr-raw-basic

Conversation

@scottlovegrove
Copy link
Copy Markdown
Collaborator

Summary

createDcrProvider's client_secret_basic path used oauth4webapi's ClientSecretBasic, which form-url-encodes the HTTP Basic credential per RFC 6749 §2.3.1. That turns reserved chars in the client_id into percent-escapes — a DCR-issued twd_… becomes twd%5F…. Servers that don't url-decode the Basic credential (e.g. Twist's token endpoint) then fail the lookup with 404 "Integration with client_id not found".

This swaps in a small raw Basic ClientAuth that base64s client_id:client_secret verbatim.

Why raw is the better default

  • A conformant server url-decodes the credential → raw twd_abc has no %-sequences, decodes to itself → works.
  • A non-conformant server (doesn't decode) → raw works; the form-encoded form breaks.

So raw is interoperable with both, for the URL-safe credentials OAuth servers issue in practice. It also matches what hand-rolled clients (and curl -u) send. Consumers whose credentials can contain : / % / + (rare) can still select tokenEndpointAuthMethod: 'client_secret_post', which sends the credential in the body via URLSearchParams.

Found while migrating twist-cli onto createDcrProvider (twist-cli#243) — Twist client_ids start with twd_, so every login hit this.

Test plan

  • npm run build — clean
  • npm test — 436 pass (DCR Basic test now uses an underscore-bearing client_id to guard the raw encoding)
  • npm run check — clean
  • Verified end-to-end via linked build: tw auth login against twist.com now completes the token exchange

🤖 Generated with Claude Code

oauth4webapi's ClientSecretBasic form-url-encodes the Basic credential per
RFC 6749 §2.3.1, turning reserved chars in the client_id into percent-escapes
(a DCR-issued `twd_…` becomes `twd%5F…`). Servers that don't url-decode the
Basic credential — Twist among them — then fail the lookup with
"client_id not found".

Replace `oauth.ClientSecretBasic` with a raw Basic `ClientAuth` that base64s
`client_id:client_secret` verbatim. Raw is interoperable with both decoding
and non-decoding servers for the URL-safe credentials OAuth servers issue in
practice; consumers whose credentials contain `:` / `%` / `+` can still select
`client_secret_post`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@scottlovegrove scottlovegrove self-assigned this May 21, 2026
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.

This PR thoughtfully replaces the standard form-url-encoded HTTP Basic authentication with a raw base64 encoding to better support non-conformant token endpoints. It provides a practical workaround that successfully eases integration for the URL-safe credentials typically issued in practice. However, there are a few edge cases to review regarding how this unencoded approach might silently break conformant credentials containing characters like colons or plus signs, and the regression test fixture could be strengthened by utilizing a character that explicitly requires escaping to properly pin the new behavior.

Share FeedbackReview Logs

Comment thread src/auth/providers/dcr.ts Outdated
Comment thread src/auth/providers/dcr.test.ts Outdated
Address review: fully-raw Basic silently breaks credentials containing
reserved chars (`:` `%` `+` `/`) — a conformant token endpoint only
reconstructs those when each component is escaped first. Encode each
component with encodeURIComponent (RFC 3986): it escapes the reserved chars
but, unlike oauth4webapi's stricter §2.3.1 form-encoding, leaves the
unreserved `-` `_` `.` `~` intact — so servers that don't url-decode the
Basic credential (a DCR-issued `twd_…` client_id) still match, with no
silently-unrepresentable header.

Regression fixture now uses a secret with `+` / `/` to pin both halves of
the contract: preserved `_`, escaped reserved chars.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@scottlovegrove scottlovegrove merged commit 97a626a into main May 21, 2026
4 checks passed
@scottlovegrove scottlovegrove deleted the scottl/dcr-raw-basic branch May 21, 2026 17:24
doist-release-bot Bot added a commit that referenced this pull request May 21, 2026
## [0.20.1](v0.20.0...v0.20.1) (2026-05-21)

### Bug Fixes

* **auth:** send DCR HTTP Basic credentials raw, not form-url-encoded ([#43](#43)) ([97a626a](97a626a))
@doist-release-bot
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 0.20.1 🎉

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.

2 participants