Skip to content

fix(auth): drop a stored credential the server rejected with 401/403 - #7732

Open
inamdarmihir wants to merge 1 commit into
google:mainfrom
inamdarmihir:fix/authenticated-tool-reauth-on-401
Open

inamdarmihir wants to merge 1 commit into
google:mainfrom
inamdarmihir:fix/authenticated-tool-reauth-on-401

Conversation

@inamdarmihir

Copy link
Copy Markdown
Contributor

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

Problem

BaseAuthenticatedTool and AuthenticatedFunctionTool only call request_credential when the credential service holds nothing. If a stored credential is later revoked or turns out to be wrong (bad password, token for the wrong account) and the API answers 401/403, it stays in the store and is replayed on every call for the rest of the session. The user can never re-authenticate.

Solution

  • New CredentialManager.discard_stored_credential(context) clears the credential kept in the credential service.
  • BaseAuthenticatedTool.run_async and AuthenticatedFunctionTool.run_async catch an exception from the tool body. If it is an HTTP 401/403 (httpx.HTTPStatusError, also found through __cause__, __context__ and exception groups), they discard the stored credential and re-raise the original error. The next call then goes through request_credential.
  • New protected hook BaseAuthenticatedTool._discard_rejected_credential for subclasses that report an unauthorized outcome as a return value.
  • Left alone: static API key / HTTP credentials, custom auth schemes (the provider owns the state), and runs without a credential service. McpTool inherits the behavior through BaseAuthenticatedTool.
  • RestApiTool keeps its existing ToolAuthHandler 401 handling and is untouched.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

New tests/unittests/tools/test_authenticated_tool_reauth.py (11 tests): 401 and 403 clear the credential and the next call requests it again (for BaseAuthenticatedTool, AuthenticatedFunctionTool and McpTool through a mocked session), a wrapped error is detected, and 404 / 500 / ValueError / successful calls leave the credential alone, as does a static API key credential.

  • With the src changes stashed: 5 failed, 6 passed.
  • With the fix: 11 passed, and tests/unittests/tools plus tests/unittests/auth give 3660 passed, 1 skipped.

Manual End-to-End (E2E) Tests:

BEFORE  after 401: stored credential still present; call 2 ran the tool with 'rejected-token' again
        impl invocations: ['rejected-token', 'rejected-token']
AFTER   after 401: stored credential = None; call 2 -> 'Pending User Authorization.'
        requested_auth_configs: ['call-1']; impl invoked once

Not run: tests outside tests/unittests/tools and tests/unittests/auth.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

Limits, so reviewers know what is and is not covered:

  • Detection needs a raised httpx.HTTPStatusError. Tools that return 401/403 as a value, and MCP failures with no httpx cause (isError=true results, a bare McpError), are not detected here. The new hook is there for them.
  • A 403 for insufficient permission also re-prompts the user, as proposed on the issue.
  • The failing call still raises. The credential is requested on the next call.
  • BaseCredentialService has no compare-and-delete, so a late 401 from a call using an old token could clear a freshly refreshed credential.

Authored by Mihir Inamdar.

BaseAuthenticatedTool and AuthenticatedFunctionTool only called
request_credential when the credential service held nothing, so a
credential that was revoked or expired stayed in the store and was
replayed on every call for the rest of the session.

When the tool body raises an HTTP 401/403 (an httpx.HTTPStatusError,
including one wrapped in another exception), clear the credential kept in
the credential service so the next call goes through request_credential
again. Static API key / HTTP credentials, custom auth schemes and runs
without a credential service are left alone, and the original error still
propagates. McpTool inherits this through BaseAuthenticatedTool.

Subclasses that report an unauthorized outcome as a return value can call
the new _discard_rejected_credential hook.

Fixes google#3016
@google-cla

google-cla Bot commented Oct 26, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@inamdarmihir

Copy link
Copy Markdown
Contributor Author

@googlebot I signed it!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BaseAuthenticatedTool saves invalid credentials

2 participants