Return *oauth2.RetrieveError from tokenexchange#5082
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5082 +/- ##
==========================================
- Coverage 67.19% 67.19% -0.01%
==========================================
Files 598 598
Lines 60258 60260 +2
==========================================
+ Hits 40493 40494 +1
- Misses 16691 16696 +5
+ Partials 3074 3070 -4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
tgrunnagle
previously approved these changes
Apr 29, 2026
Replace the private oAuthError type with *oauth2.RetrieveError from golang.org/x/oauth2 so token exchange errors expose RFC 6749 §5.2 fields (error, error_description, error_uri) as structured data via errors.As. This is the library-standard surface for non-2xx token endpoint responses, and aligns the error shape with the JWT Bearer grant that will share helpers in pkg/oauth. Behavior changes: - validateResponseStatus takes *http.Response so it can attach the full response to the returned error and parse the body as RFC 6749 §5.2 best-effort. - When the body is non-conformant (no "error" field, e.g. a proxy HTML 5xx), the raw body is logged at debug level and cleared from the returned error. This prevents oauth2.RetrieveError.Error() from interpolating arbitrary upstream content (HTML, hostnames, stack traces) into wrapped error strings — same two-tier pattern used by formatOAuth2Error in pkg/authserver. - parseTokenExchangeResponse wraps json.Unmarshal failures with %w. The error type change is isolated from code movement so a future bisect can distinguish "error shape regressed" from "plumbing regressed".
The previous commit cleared the body only when the response was non-conformant (no RFC 6749 §5.2 "error" field), on the theory that a structured-error body is bounded and harmless. PR review pointed out the asymmetry, and the simpler answer is to clear Body in both branches: - The structured fields (ErrorCode, ErrorDescription, ErrorURI) are already extracted onto *oauth2.RetrieveError, so callers using errors.As lose nothing. - Full body content is preserved in slog.Debug for ops, regardless of which branch is taken. - No caller in this repo reads retrieveErr.Body for any non-debug purpose (verified by grep on .RetrieveError\b). - Removes a special case future maintainers would have to re-derive. This matches Ory Hydra, which never surfaces raw upstream error bodies through its public error type — see the JWKS fetcher in fosite/client_authentication_jwks_strategy.go and the token-hook client in oauth2/token_hook.go, both of which discard the body and return only the upstream status code on non-2xx responses.
1f32fdf to
e8109cc
Compare
ChrisJBurns
approved these changes
May 5, 2026
tgrunnagle
approved these changes
May 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.
Summary
oAuthErrorwhose only output is a formatted string. Callers cannot inspect the RFC 6749 §5.2 fields without parsing the message. This PR aligns the error shape with the upcoming JWT Bearer grant, both of which will share helpers inpkg/oauth—*oauth2.RetrieveErrorfromgolang.org/x/oauth2is the library-standard surface for non-2xx token endpoint responses.oAuthErrorwith*oauth2.RetrieveError.validateResponseStatusnow takes*http.Responseso it can attach the full response and best-effort-parse the body. On non-conformant bodies (noerrorfield, e.g. proxy HTML 5xx), the raw body is logged at debug level and cleared from the returned error soRetrieveError.Error()cannot interpolate arbitrary upstream content (HTML, hostnames, stack traces) into wrapped log strings — same two-tier pattern already used byformatOAuth2Errorinpkg/authserver.parseTokenExchangeResponsewrapsjson.Unmarshalfailures with%w.Type of change
(Note: the body-redaction defense above is a deliberate hardening of an information-disclosure path the previous code already had via the implicit fmt error string; user-visible structured behavior is unchanged.)
Test plan
task test) —pkg/auth/tokenexchangepasses; two pre-existing failures onmainin unrelated packages (pkg/skills/clienthardcoded port,pkg/workloadsempty-group filter) are not introduced by this PR.task lint-fix) — 0 issues.TestExchangeToken_HTTPErrorResponseswas migrated from substring matching onerr.Error()toerrors.As(err, &*oauth2.RetrieveError{})and now asserts onResponse.StatusCode,ErrorCode,ErrorDescription, andBody(preserved on conformant responses, cleared on the 503 non-JSON case).API Compatibility
v1beta1API.Does this introduce a user-facing change?
No. Internal error type change. Operators who scrape the existing
slog.Debuglog fields (oauth_error_code,description,status,body_length) will see identical field names; the body-content debug field is new and additive.Special notes for reviewers
pkg/oauthshared-helpers migration (subsequent commits 5b/5c) so a future bisect can distinguish "error shape regressed" from "plumbing regressed."pkg/vmcp/auth/strategies/tokenexchange.go:142wraps with%w, soerrors.Ascontinues to work and the leading"token exchange failed: "prefix is preserved.formatOAuth2Errorprecedent.Generated with Claude Code