Skip to content

Commit a6d035c

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

File tree

1 file changed

+47
-0
lines changed

1 file changed

+47
-0
lines changed

tests/test_application.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -875,3 +875,50 @@ 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+
def test_token_should_be_cached_with_response_scope(self):
884+
"""Based on https://datatracker.ietf.org/doc/html/rfc6749#section-3.3
885+
authorization server may issue an access token with different scope.
886+
For example, eSTS normalizes scopes by adding or removing trailing slash.
887+
Calling app is supposed to use the normalized scope for subsequent calls.
888+
"""
889+
890+
# Create a fresh app instance
891+
app = ConfidentialClientApplication(
892+
"client_id", client_credential="secret",
893+
authority="https://login.microsoftonline.com/common")
894+
895+
# Mocked request: ask for "invalid_scope" scope but receive "valid_scope1 valid_scope2" scope in response
896+
def mock_post(url, headers=None, *args, **kwargs):
897+
return MinimalResponse(status_code=200, text=json.dumps({
898+
"access_token": "AT_with_valid_scope1_valid_scope2_scopes",
899+
"expires_in": 3600,
900+
"scope": "valid_scope1 valid_scope2", # Response scope differs from requested scope
901+
"token_type": "Bearer"
902+
}))
903+
904+
result1 = app.acquire_token_for_client(["invalid_scope"], post=mock_post)
905+
self.assertEqual(result1[app._TOKEN_SOURCE], app._TOKEN_SOURCE_IDP)
906+
self.assertEqual("AT_with_valid_scope1_valid_scope2_scopes", result1.get("access_token"))
907+
self.assertEqual(["valid_scope1", "valid_scope2"], result1.get("scope").split()) # Scope from response
908+
909+
# Second request: ask for same "invalid_scope" scope again
910+
# Since cached token has "valid_scope1 valid_scope2" scopes, it shouldn't match the "invalid_scope" request
911+
# This should go to IDP again and receive the same response
912+
result2 = app.acquire_token_for_client(["invalid_scope"], post=mock_post)
913+
# Should get a new token from IDP, not from cache
914+
self.assertEqual(result2[app._TOKEN_SOURCE], app._TOKEN_SOURCE_IDP)
915+
self.assertEqual("AT_with_valid_scope1_valid_scope2_scopes", result2.get("access_token"))
916+
self.assertEqual(["valid_scope1", "valid_scope2"], result2.get("scope").split())
917+
918+
# Third and fourth requests: ask for individual valid scopes
919+
# Should hit cache for the token that has "valid_scope1 valid_scope2" scopes
920+
for scope in ["valid_scope1", "valid_scope2"]:
921+
result = app.acquire_token_for_client([scope])
922+
self.assertEqual(result[app._TOKEN_SOURCE], app._TOKEN_SOURCE_CACHE)
923+
self.assertEqual("AT_with_valid_scope1_valid_scope2_scopes", result.get("access_token"))
924+
self.assertIsNone(result.get("scope"), "scope field is not returned when token comes from cache")

0 commit comments

Comments
 (0)