Skip to content

Commit d1a9717

Browse files
committed
Demonstrates the behavior of mismatching scope
1 parent f803aec commit d1a9717

File tree

1 file changed

+49
-0
lines changed

1 file changed

+49
-0
lines changed

tests/test_application.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,3 +875,52 @@ def test_app_did_not_register_redirect_uri_should_error_out(self):
875875
parent_window_handle=app.CONSOLE_WINDOW_HANDLE,
876876
)
877877
self.assertEqual(result.get("error"), "broker_error")
878+
879+
880+
class MismatchingScopeTestCase(unittest.TestCase):
881+
"""Test cache behavior when HTTP response scope differs from requested scope"""
882+
883+
@classmethod
884+
@patch(_OIDC_DISCOVERY, new=_OIDC_DISCOVERY_MOCK)
885+
def setUpClass(cls):
886+
cls.app = ConfidentialClientApplication(
887+
"client_id", client_credential="secret",
888+
authority="https://login.microsoftonline.com/common")
889+
890+
def test_mismatching_scope_should_be_cached_with_response_scope(self):
891+
"""Based on https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
892+
authorization server may issue an access token with different scope.
893+
For example, eSTS normalizes scopes by adding or removing trailing slash.
894+
Calling app is supposed to use the normalized scope for subsequent calls.
895+
"""
896+
897+
# Mocked request: ask for "foo" scope but receive "bar" scope in response
898+
def mock_post(url, headers=None, *args, **kwargs):
899+
return MinimalResponse(status_code=200, text=json.dumps({
900+
"access_token": "AT_with_bar_scope",
901+
"expires_in": 3600,
902+
"scope": "bar", # Response scope differs from requested scope
903+
"token_type": "Bearer"
904+
}))
905+
906+
result1 = self.app.acquire_token_for_client(["foo"], post=mock_post)
907+
self.assertEqual(result1[self.app._TOKEN_SOURCE], self.app._TOKEN_SOURCE_IDP)
908+
self.assertEqual("AT_with_bar_scope", result1.get("access_token"))
909+
self.assertEqual(["bar"], result1.get("scope").split()) # Scope from response
910+
911+
# Second request: ask for same "foo" scope again
912+
# Since cached token has "bar" scope, it shouldn't match the "foo" request
913+
# This should go to IDP again and receive the same response
914+
result2 = self.app.acquire_token_for_client(["foo"], post=mock_post)
915+
# Should get a new token from IDP, not from cache
916+
self.assertEqual(result2[self.app._TOKEN_SOURCE], self.app._TOKEN_SOURCE_IDP)
917+
self.assertEqual("AT_with_bar_scope", result2.get("access_token"))
918+
self.assertEqual(["bar"], result2.get("scope").split())
919+
920+
# Third request: ask for "bar" scope
921+
# Should hit cache for the token that had "bar" scope
922+
result3 = self.app.acquire_token_for_client(["bar"])
923+
self.assertEqual(result3[self.app._TOKEN_SOURCE], self.app._TOKEN_SOURCE_CACHE)
924+
self.assertEqual("AT_with_bar_scope", result3.get("access_token"))
925+
# Implementation detail: scope field is not returned when token comes from cache
926+
self.assertIsNone(result3.get("scope"))

0 commit comments

Comments
 (0)