Skip to content

fix(auth): keep the consent URI out of ConsentRequiredError's message - #1519

Merged
wolo-lab merged 2 commits into
mainfrom
wolo/auth-consent-uri
Sep 7, 2026
Merged

wolo-lab merged 2 commits into
mainfrom
wolo/auth-consent-uri

Conversation

@wolo-lab

@wolo-lab wolo-lab commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Problem

A *ConsentRequiredError becomes a tool's error, which is fed back to the model and persisted as a session event. Its message carried AuthURI, and that URI carries the state and the consent nonce that bind the credential. A 3-legged authorization URI also normally carries the acting user in a login_hint parameter, so the message published an end-user identifier as well.

Summary

Error() no longer prints the URI. It stays on the AuthURI field, which is how consumers already read it — the only assertion on the message text anywhere in the tree was the one updated here.

AuthURI and Nonce now say on the field that they go to the acting user and nowhere else: not a log, not a span, and not through json.Marshal, which publishes them whatever Error() returns. AuthURI is deliberately not redacted the way service text is in other errors, because the identifier in it is what makes the URL work.

apidiff reports the change as additive, since no signature moved and the message text is not part of what it compares.


First of a five-part split of #1173. Nothing is stacked below this one.

@karolpiotrowicz karolpiotrowicz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing blocks. The change does what it says and the evidence for it is unusually tight — one nit inline, and two merge preconditions that are not about the code.

The premise checks out end to end. base_flow.go:1416 turns a tool error into map[string]any{"error": err.Error()}, and that map is the FunctionResponse payload the model reads and the session store keeps, so the message really was the leak channel. Worth stating precisely, though: the producer side, auth/gcp client.go:215, has no non-test caller anywhere in this repository, so the two halves are only joined by a downstream integrator wiring a provider through mcptoolset.Config.Auth. Both halves are real. The join is out of tree, which is the normal shape for a library and does not weaken the fix.

Applying only your three test files onto the merge-base fails on the URI in two independent places — the direct message and the %w wrap at transport.go:61 — and both pass with the production change. Those are the only two observable message strings in the repo, and exact equality pins both. apidiff is empty on ./auth and on ./auth/gcp, against the same base CI will compute.

Three things I looked at hard and came away satisfied with, since a reviewer is more likely to argue the other way on each:

  • Exact equality over a negative check is right for a stronger reason than the comment gives. A URI-substring check would not catch a Nonce leak at all — the fixture nonce is "n". The exact check catches a nonce leak, a key leak, and any field added later.
  • Documenting the json.Marshal residue instead of enforcing it looks like the only in-scope option. Unexporting the fields breaks every consumer reading them off errors.As, and a redacting MarshalJSON would also close the resume path for anything round-tripping the error. Nothing in the tree marshals it today: no log/slog anywhere, every %#v site is a test, and all four writers of map[string]any{"error": …} go through err.Error() first.
  • Not redacting AuthURI the way service text is redacted reads as coherent doctrine rather than an inconsistency. The other errors sanitize text that is hostile%q plus a length cap. This one is sensitive, and omission is the stronger treatment.

Two preconditions before merge, neither a code issue: the branch is behind main, and it conflicts with open #1465 on auth/gcp/client_test.go. Merge order decides who rebases.

On the sibling-implementation question: adk-python has no counterpart error. It signals consent by returning an AuthCredential that carries auth_uri rather than by raising, so there is no message there that could leak the URI — auth_credential.py:109, at b0180620. One detail from the same file that may interest you: on that model client_secret and private_key are declared Annotated[..., Field(repr=False)] and auth_uri is not, so the URI is not treated as secret-bearing there. Nothing for this PR to do about it — it just means the doctrine you are setting here is new rather than inherited.

Comment thread auth/providers_test.go
A *ConsentRequiredError reaches the model as a tool's error and is persisted
as a session event. Its message carried AuthURI, and that URI carries the
state and the consent nonce that bind the credential. A 3-legged
authorization URI also normally carries the acting user in a login_hint
parameter.

Drop the URI from Error(). It stays on the AuthURI field, which is how
consumers already read it, and the only assertion on the message text in the
tree was the test updated here. Document on the fields that AuthURI and
Nonce go to the acting user and nowhere else — not a log, not a span, and
not through json.Marshal, which publishes them whatever Error() says.
…tic readable

Two gaps that only appear once Error() stops carrying the URI.

Transport.RoundTrip builds a new message around the provider's error, so the
invariant lives in two places and only one of them was asserted. Reintroducing
the URI at the wrap site left the whole auth package green while the caller
received the state, the nonce and the login_hint. Asserted exactly rather than
by absence of the URI: a negative check goes vacuous the moment the fixture
above it changes.

TestRetrieveCredential printed the consent error with %+v, which resolves
through Error() rather than reflecting over the fields. With Error() now a
constant, a mismatch reported neither the AuthURI nor the Nonce it actually got.
@wolo-lab
wolo-lab force-pushed the wolo/auth-consent-uri branch from f03f973 to 6466509 Compare September 7, 2026 20:38
@wolo-lab
wolo-lab merged commit 53a0bc0 into main Sep 7, 2026
14 checks passed
@wolo-lab wolo-lab added the v2 For PRs targeting main branch. label Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 For PRs targeting main branch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants