Repository navigation
Conversation
Use the existing async OAuth exchanger for client-credentials-only schemes before the interactive authorization path. Reuse unexpired session tokens and obtain a new client token when they expire. Keep client secrets out of the session token cache and preserve authorization-code consent. Fixes google#3753
There was a problem hiding this comment.
Code Review
This pull request introduces support for the OAuth2 client credentials flow in ToolAuthHandler, enabling token exchange without user interaction, as well as token caching, expiration handling, and eviction on authorization errors. It also adds comprehensive unit tests for these scenarios. The feedback suggests adding a safety check in _is_client_credentials_only to prevent a potential AttributeError if flows is None.
| def _is_client_credentials_only(self) -> bool: | ||
| if not isinstance(self.auth_scheme, OAuth2): | ||
| return False | ||
| flows = self.auth_scheme.flows | ||
| return bool( | ||
| flows.clientCredentials | ||
| and not (flows.authorizationCode or flows.implicit or flows.password) | ||
| ) |
There was a problem hiding this comment.
The flows attribute of OAuth2 can be None or falsy if the security scheme is not fully or correctly populated. Accessing flows.clientCredentials directly without checking if flows is None will raise an AttributeError. Adding a check to ensure flows is present before accessing its attributes prevents potential runtime crashes.
| def _is_client_credentials_only(self) -> bool: | |
| if not isinstance(self.auth_scheme, OAuth2): | |
| return False | |
| flows = self.auth_scheme.flows | |
| return bool( | |
| flows.clientCredentials | |
| and not (flows.authorizationCode or flows.implicit or flows.password) | |
| ) | |
| def _is_client_credentials_only(self) -> bool: | |
| if not isinstance(self.auth_scheme, OAuth2) or not self.auth_scheme.flows: | |
| return False | |
| flows = self.auth_scheme.flows | |
| return bool( | |
| flows.clientCredentials | |
| and not (flows.authorizationCode or flows.implicit or flows.password) | |
| ) |
There was a problem hiding this comment.
OAuth2.flows is a required OAuthFlows model in FastAPI. Missing and None values both fail validation, while flows={} creates an empty model that this predicate already handles. I checked those cases, so I’m keeping the validated-model contract here.
|
The final commit, 8d9f57d, passed all 16 jobs in the fork validation run, including Python 3.11–3.14, compatibility checks, formatting, and the baseline type comparison. The Runner flow also passed from source and a freshly installed wheel. Could a maintainer approve the CI run and rerun the triage job? The triage job stopped on Gemini |
Link to Issue or Description of Change
Fixes #3753. I shared the current reproduction and proposed scope on the issue.
An OpenAPI tool configured with only the OAuth2 client-credentials flow still emits
adk_request_credential, with a login URL built from the token endpoint. The token endpoint and target API are never called. The standalone async OAuth exchanger already supports this grant; the legacy OpenAPI auth path asks for user authorization before reaching it.This uses that exchanger for client-credentials-only schemes, off the event loop. It reuses valid cached tokens and obtains a new client token after expiry. A rejected token is removed so a later call can obtain a new one; it does not fall back to interactive consent. The client secret stays in the configured credential and is omitted from the session token cache. Authorization-code and mixed-flow schemes keep their existing consent path.
Testing Plan
8d9f57d.Full unit suites passed 19,132 tests on Python 3.11, 19,123 tests on Python 3.12, 19,123 tests on Python 3.13, 19,123 tests on Python 3.14. Existing skips and expected-failure results are recorded in the run.
The uncached mypy comparison found no new diagnostics on Python 3.11–3.13 (3.11: 538 baseline and 538 branch errors, 3.12: 538 baseline and 538 branch errors, 3.13: 538 baseline and 538 branch errors).
The new regression file fails six cases on unmodified main. Self-review also caught a rejected token remaining cached in the first version; the regression reproduced that failure before the invalidation fix. The linked full validation is for the corrected final commit. The fork workflow excludes
lancedbfrom dependency installation.Manual End-to-End (E2E) Tests:
Ran a real
OpenAPIToolsetandRunnerwith a fake model and fake HTTP endpoints, from source and from a wheel installed into a fresh environment. Both issue a token POST withgrant_type=client_credentials, call the API with the returned bearer token, and emit noadk_request_credential. The loaded auth module has the same hash in the source and wheel runs.The reproduction script and saved results are on a separate fork branch. Run the first command with the source checkout and the second with the fresh wheel environment's Python:
Checklist