-
Notifications
You must be signed in to change notification settings - Fork 48
OAuth2: key token caches by the full credential configuration #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
1093ab4
oauth: key token caches by the full credential configuration
h3xxit 28f35b1
chore: bump versions; plugins require utcp>=1.1.4
h3xxit 84c1c24
ci: run the file, websocket and gql plugin suites
h3xxit d452258
test: give the OAuth cache-isolation tests plugin-prefixed basenames
h3xxit File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| """``OAuth2Auth.cache_key``: the identity of a credential configuration. | ||
|
|
||
| This key is the single source of the rule that every communication protocol | ||
| uses to cache and coalesce tokens, so its semantics are pinned here: two | ||
| configurations share a key exactly when they would obtain the same token. | ||
| """ | ||
|
|
||
| from utcp.data.auth_implementations import OAuth2Auth | ||
|
|
||
|
|
||
| def _auth(**overrides) -> OAuth2Auth: | ||
| fields = dict( | ||
| auth_type="oauth2", | ||
| token_url="https://issuer-a.example/token", | ||
| client_id="client", | ||
| client_secret="secret", | ||
| scope="read", | ||
| ) | ||
| fields.update(overrides) | ||
| return OAuth2Auth(**fields) | ||
|
|
||
|
|
||
| def test_identical_configurations_share_a_key(): | ||
| assert _auth().cache_key() == _auth().cache_key() | ||
|
|
||
|
|
||
| def test_every_token_affecting_field_changes_the_key(): | ||
| base = _auth().cache_key() | ||
| assert _auth(token_url="https://issuer-b.example/token").cache_key() != base | ||
| assert _auth(client_id="other").cache_key() != base | ||
| assert _auth(client_secret="other").cache_key() != base | ||
| assert _auth(scope="write").cache_key() != base | ||
|
|
||
|
|
||
| def test_same_client_id_at_a_different_issuer_does_not_share_a_key(): | ||
| # The flaw this rule exists to prevent: a shared client_id must never let | ||
| # two configurations receive each other's tokens. | ||
| a = _auth(token_url="https://issuer-a.example/token") | ||
| b = _auth(token_url="https://issuer-b.example/token") | ||
| assert a.client_id == b.client_id | ||
| assert a.cache_key() != b.cache_key() | ||
|
|
||
|
|
||
| def test_absent_scope_normalises_to_empty(): | ||
| assert _auth(scope=None).cache_key() == _auth(scope="").cache_key() |
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
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
30 changes: 30 additions & 0 deletions
30
plugins/communication_protocols/gql/tests/test_gql_oauth_cache_isolation.py
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| """OAuth2 token cache is isolated per credential configuration. | ||
|
|
||
| The token cache is keyed by ``OAuth2Auth.cache_key``, so two templates that | ||
| share a ``client_id`` but differ in issuer, secret or scope never receive each | ||
| other's tokens. Network-free: the cache is seeded directly, and the second | ||
| configuration is only checked for absence. | ||
| """ | ||
|
|
||
| import pytest | ||
|
|
||
| from utcp.data.auth_implementations import OAuth2Auth | ||
| from utcp_gql.gql_communication_protocol import GraphQLCommunicationProtocol | ||
|
|
||
|
|
||
| def _auth(token_url: str) -> OAuth2Auth: | ||
| return OAuth2Auth( | ||
| auth_type="oauth2", token_url=token_url, client_id="shared", client_secret="s", scope="" | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_token_cache_is_isolated_per_credential_configuration(): | ||
| proto = GraphQLCommunicationProtocol() | ||
| a = _auth("https://issuer-a.example/token") | ||
| b = _auth("https://issuer-b.example/token") # same client_id, different issuer | ||
|
|
||
| proto._oauth_tokens[a.cache_key()] = {"access_token": "token-for-a"} | ||
|
|
||
| assert await proto._handle_oauth2(a) == "token-for-a" | ||
| assert b.cache_key() not in proto._oauth_tokens |
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
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
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
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
39 changes: 39 additions & 0 deletions
39
plugins/communication_protocols/http/tests/test_http_oauth_cache_isolation.py
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| """OAuth2 token caches are isolated per credential configuration. | ||
|
|
||
| Each HTTP-family protocol keys its token cache by ``OAuth2Auth.cache_key``, | ||
| so two templates that share a ``client_id`` but differ in issuer, secret or | ||
| scope never receive each other's tokens. Network-free: the cache is seeded | ||
| directly, and the second configuration is only checked for absence. | ||
| """ | ||
|
|
||
| import pytest | ||
|
|
||
| from utcp.data.auth_implementations import OAuth2Auth | ||
| from utcp_http.http_communication_protocol import HttpCommunicationProtocol | ||
| from utcp_http.sse_communication_protocol import SseCommunicationProtocol | ||
| from utcp_http.streamable_http_communication_protocol import StreamableHttpCommunicationProtocol | ||
|
|
||
|
|
||
| def _auth(token_url: str) -> OAuth2Auth: | ||
| return OAuth2Auth( | ||
| auth_type="oauth2", token_url=token_url, client_id="shared", client_secret="s", scope="" | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "protocol_class", | ||
| [HttpCommunicationProtocol, SseCommunicationProtocol, StreamableHttpCommunicationProtocol], | ||
| ) | ||
| @pytest.mark.asyncio | ||
| async def test_token_cache_is_isolated_per_credential_configuration(protocol_class): | ||
| proto = protocol_class() | ||
| a = _auth("https://issuer-a.example/token") | ||
| b = _auth("https://issuer-b.example/token") # same client_id, different issuer | ||
|
|
||
| proto._oauth_tokens[a.cache_key()] = {"access_token": "token-for-a"} | ||
|
|
||
| # A is served from the cache (proves the key is what the lookup uses)... | ||
| assert await proto._handle_oauth2(a) == "token-for-a" | ||
| # ...and B, sharing only the client_id, has no entry and so can never be | ||
| # handed A's token. | ||
| assert b.cache_key() not in proto._oauth_tokens |
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: This run line adds gql/tests/ to the 9-matrix CI jobs, and test_graphql_integration.py hits the external Countries API (countries.trevorblades.com) unconditionally with no offline/skip guard. If that public API is unreachable, down, or rate-limited, every matrix job fails and blocks all PRs. The PR description already notes these tests can time out under parallel load. Guard the network-dependent tests to skip when network is unavailable (e.g., a socket-gated skipif marker) or move them to a separate non-blocking job, so pipeline health does not depend on an external service.
Prompt for AI agents