Add in-memory caching for token introspection results#3298
Add in-memory caching for token introspection results#3298
Conversation
🤖 Generated with Claude Code https://claude.ai/code/session_0172r9LV6i22XgFyLmziaKhk
…iry tests 🤖 Generated with Claude Code https://claude.ai/code/session_0172r9LV6i22XgFyLmziaKhk
🤖 Generated with Claude Code https://claude.ai/code/session_0172r9LV6i22XgFyLmziaKhk
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f0e353c5db
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if len(self._cache) >= self._max_cache_size: | ||
| self._cleanup_expired_cache() | ||
| # If still at limit after cleanup, evict oldest entry | ||
| if len(self._cache) >= self._max_cache_size: | ||
| oldest_key = next(iter(self._cache)) |
There was a problem hiding this comment.
Reject non-positive max_cache_size values
When caching is enabled and max_cache_size is set to 0 (or any negative value), the size check is immediately true even for an empty cache, so next(iter(self._cache)) raises StopIteration. That exception is then swallowed by the broad except Exception in verify_token, causing otherwise valid introspection responses to return None and reject tokens. Validate max_cache_size as a positive integer (or treat non-positive values as cache-disabled) before entering this eviction path.
Useful? React with 👍 / 👎.
🤖 Generated with Claude Code https://claude.ai/code/session_0172r9LV6i22XgFyLmziaKhk
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fc8a7ca860
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if not introspection_data.get("active", False): | ||
| self.logger.debug("Token introspection returned active=false") | ||
| self._set_cached(token, None) | ||
| return None |
There was a problem hiding this comment.
Avoid caching every
active=false introspection result
This branch caches None for any active=false response, and later calls short-circuit on that cache entry without re-introspecting. In environments where a token can transition from inactive to active (for example, tokens with a future nbf or short-lived authorization-server propagation delays), the verifier will continue rejecting an otherwise valid token for the full cache TTL. Consider not caching active=false results, or at least bounding that cache lifetime by a claim like nbf when present.
Useful? React with 👍 / 👎.
🤖 Generated with Claude Code https://claude.ai/code/session_0172r9LV6i22XgFyLmziaKhk
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3dbb4e4977
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| del self._cache[cache_key] | ||
| return (False, None) # Expired | ||
|
|
||
| return (True, entry.result) |
There was a problem hiding this comment.
Return a copy of cached AccessToken values
When caching is enabled, this returns the exact same AccessToken instance for every cache hit. AccessToken contains mutable fields (claims dict and scopes list), so if any request-path code mutates them (for example adding derived claims), that mutation is persisted in the cache and leaks into later requests for the same bearer token, which can change downstream authorization behavior. Please return a defensive copy (or cache an immutable representation) instead of sharing the same object instance across requests.
Useful? React with 👍 / 👎.
🤖 Generated with Claude Code https://claude.ai/code/session_0172r9LV6i22XgFyLmziaKhk
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: be4c9074c7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # Cache scope failures - token is valid but doesn't have required scopes | ||
| self._set_cached(token, None) |
There was a problem hiding this comment.
Avoid caching scope-mismatch denials
verify_token now caches None when required_scopes are missing, and later _get_cached short-circuits without re-introspection, so a token that is initially denied due to transient introspection inconsistency (or dynamic scope/permission propagation) will keep failing for the full cache TTL even after the auth server would return sufficient scopes. This creates sticky false negatives in deployments where introspection data can change shortly after issuance.
Useful? React with 👍 / 👎.
Never cache None - failures (inactive, expired, missing scopes, errors) should not create sticky false negatives. The cache now only stores valid AccessToken results. 🤖 Generated with Claude Code https://claude.ai/code/session_0172r9LV6i22XgFyLmziaKhk
🤖 Generated with Claude Code https://claude.ai/code/session_0172r9LV6i22XgFyLmziaKhk
Token introspection validates every request against the authorization server, which can become a bottleneck under load. This adds optional in-memory caching to
IntrospectionTokenVerifierthat reduces introspection calls while respecting token lifecycles.Caching is disabled by default to preserve real-time revocation semantics—if you need instant token invalidation, the default behavior is unchanged. When enabled, the cache respects the token's own expiration claim, so a token expiring in 30 seconds won't be cached for 5 minutes.
Closes #3285
Note: for the moment, this is an in-memory cache, not full pykeyvalue compliance.