test(auth): make the revocation fixture a real form-urldecoder - #2246
Merged
Conversation
#2222 reported that `core/auth/revocation.ts` breaks a client secret containing `+`, on the basis that `encodeURIComponent` leaves `+` bare. It does not — it escapes it as `%2B`. The characters it leaves bare are exactly `!'()*-._~` and alphanumerics, every one of which a form- urldecoder passes through unchanged, so its output decodes identically under both algorithms; verified over every code point up to U+2FFF. Switching to `URLSearchParams` would be the literal algorithm §2.3.1 names and a small regression: it encodes a space as `+`, which a lenient server decoding with `decodeURIComponent` alone reads as a literal `+`, while `%20` is understood by both. The encoder is therefore unchanged and the reasoning is recorded on it. The report's second half stands and is the real defect: the fixture decoded with `decodeURIComponent`, the encoder's own inverse, so the round trip succeeded for every input by construction and no test could have failed on an encoding mistake. `/oauth/revoke` now runs a genuine RFC 6749 Appendix B decode, with unit cases for `+`, a space, `:` and `%` (asserted on the wire and through an independent decoder), an end-to-end client whose secret is base64 with a `+` in it, and a guard that an unencoded credential is refused — the case that separates the two decoders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BhVaufrEZAfFAXcg6456hK Signed-off-by: cliffhall <cliff@futurescale.com>
There was a problem hiding this comment.
🟢 Approval recommended
The fixture correction is standards-aligned and covered by focused unit and integration tests.
Pull request overview
Improves revocation credential testing by making the OAuth fixture decode form-encoded Basic credentials correctly.
Changes:
- Adds compliant form URL decoding to the revocation fixture.
- Adds unit and integration coverage for reserved characters.
- Documents why
encodeURIComponentremains appropriate.
File summaries
| File | Description |
|---|---|
test-servers/src/test-server-oauth.ts |
Corrects credential decoding. |
core/auth/revocation.ts |
Documents encoding rationale. |
clients/web/src/test/core/auth/revocation.test.ts |
Adds wire-format and round-trip tests. |
clients/web/src/test/integration/auth/revocation-e2e.test.ts |
Adds end-to-end + credential coverage. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟢 Approval recommended
The fixture now independently models compliant server decoding, and the expanded tests directly distinguish correct encoding from the previous self-inverse setup.
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 0 new
- Review effort level: Balanced
This was referenced Sep 5, 2026
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.
Closes #2222
The report's premise turned out to be wrong, and that changed the fix
#2222 says
core/auth/revocation.tsbreaks a client secret containing+, becauseencodeURIComponentleaves+bare and a form-urldecoder reads a bare+as a space. It does not:encodeURIComponentleaves exactly!'()*-._~and alphanumerics unescaped, and a form-urldecoder passes every one of those through unchanged — so it can never emit the one character the two algorithms disagree about. Its output decodes identically under both. Checked exhaustively rather than argued:formUrlDecode(encodeURIComponent(s)) === sfor every code point up to U+2FFF, zero mismatches.The proposed switch to
URLSearchParamswould be a small regression. It is the literal algorithm §2.3.1 names, but it encodes a space as+— which a compliant server reads back as a space and a lenient one (decoding withdecodeURIComponentalone) reads as a literal+.%20is understood by both:decodeURIComponent) serverab cdencodeURIComponent→ab%20cdab cdab cdab cdURLSearchParams→ab+cdab cdab+cdab+cdencodeURIComponent→ab%2Bcdab+cdab+cdSo the encoder is unchanged, and the reasoning is recorded in a comment on it — the next reader will have the same doubt, and the answer is not obvious from the line itself.
What was genuinely broken: the fixture was the encoder's own inverse
The second half of the report stands, and is the better bug.
test-servers'/oauth/revokedecoded withdecodeURIComponent, the exact inverse of the encoder under test, so the round trip succeeded for every input by construction. No test in either suite could have failed on an encoding mistake — the suite's apparent coverage of RFC 6749 §2.3.1 client authentication was really a statement that the encoder is self-consistent with itself.Changes
test-servers/src/test-server-oauth.ts—/oauth/revokenow runs a real Appendix B decode (+→ space before percent-decoding; doing it after would turn a legitimately escaped%2Binto a space). The existingtry/catchis kept, so a malformed escape is still aninvalid_client401 rather than an Express 500.clients/web/src/test/core/auth/revocation.test.ts— cases for a credential containing+, a space,:and%, each asserted twice: the exact wire bytes (which encoder ran) and a round trip through an independent compliant decoder (what a real server gets back). The decoder is written out inline rather than imported, so the assertion does not lean on the fixture it exists to corroborate.clients/web/src/test/integration/auth/revocation-e2e.test.ts— a third static client whose secret is base64 with a+in it, revoked end to end against the real fixture; plus a guard that an unencoded credential with a+is now refused, which is the one case that separates the two decoders and the assertion that goes red if anyone reverts the fixture.core/auth/revocation.ts— comment only. No behavior change.The existing surrogate case was also tightened: it asserted only
status: "failed", which a stub returning nothing satisfies anyway (reading.okoffundefinedthrows into the samecatch). It now asserts the fetch was never called, so it is about the encoder rather than about the stub.Mutation-checked
Each new guard was removed in turn to confirm it detects rather than merely passes:
decodeURIComponent(the old behavior)+(the bug as reported)npm run local:gatepasses. No UI change, so no screenshots.🤖 Generated with Claude Code
https://claude.ai/code/session_01BhVaufrEZAfFAXcg6456hK