@@ -875,3 +875,50 @@ def test_app_did_not_register_redirect_uri_should_error_out(self):
875
875
parent_window_handle = app .CONSOLE_WINDOW_HANDLE ,
876
876
)
877
877
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