async def test_persistent_cache_multiple_clients(cert_path, cert_password): """the credential shouldn't use tokens issued to other service principals""" access_token_a = "token a" access_token_b = "not " + access_token_a transport_a = async_validating_transport( requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))] ) transport_b = async_validating_transport( requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))] ) cache = TokenCache() with patch("azure.identity._internal.persistent_cache._load_persistent_cache") as mock_cache_loader: mock_cache_loader.return_value = Mock(wraps=cache) credential_a = CertificateCredential( "tenant", "client-a", cert_path, password=cert_password, enable_persistent_cache=True, transport=transport_a ) assert mock_cache_loader.call_count == 1, "credential should load the persistent cache" credential_b = CertificateCredential( "tenant", "client-b", cert_path, password=cert_password, enable_persistent_cache=True, transport=transport_b ) assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" # A caches a token scope = "scope" token_a = await credential_a.get_token(scope) assert token_a.token == access_token_a assert transport_a.send.call_count == 1 # B should get a different token for the same scope token_b = await credential_b.get_token(scope) assert token_b.token == access_token_b assert transport_b.send.call_count == 1
def test_user_agent(): client_id = "client-id" transport = validating_transport( requests=[Request()] * 2 + [Request(required_headers={"User-Agent": USER_AGENT})], responses=[ get_discovery_response(), mock_response( json_payload={ "device_code": "_", "user_code": "user-code", "verification_uri": "verification-uri", "expires_in": 42, }), mock_response(json_payload=dict(build_aad_response( access_token="**", id_token=build_id_token(aud=client_id)), scope="scope")), ], ) credential = DeviceCodeCredential(client_id=client_id, prompt_callback=Mock(), transport=transport, _cache=TokenCache()) credential.get_token("scope")
def __init__(self, tenant_id, client_id, authority=None, cache=None, **kwargs): # type: (str, str, Optional[str], Optional[TokenCache], **Any) -> None authority = normalize_authority(authority) if authority else get_default_authority() self._token_endpoint = "/".join((authority, tenant_id, "oauth2/v2.0/token")) self._cache = cache or TokenCache() self._client_id = client_id self._pipeline = self._build_pipeline(**kwargs)
def __init__(self, tenant_id, client_id, certificate_path, **kwargs): # type: (str, str, str, **Any) -> None if not certificate_path: raise ValueError( "'certificate_path' must be the path to a PEM file containing an x509 certificate and its private key" ) super(CertificateCredentialBase, self).__init__() password = kwargs.pop("password", None) if isinstance(password, six.text_type): password = password.encode(encoding="utf-8") with open(certificate_path, "rb") as f: pem_bytes = f.read() self._certificate = AadClientCertificate(pem_bytes, password=password) enable_persistent_cache = kwargs.pop("_enable_persistent_cache", False) if enable_persistent_cache: allow_unencrypted = kwargs.pop("_allow_unencrypted_cache", False) cache = load_service_principal_cache(allow_unencrypted) else: cache = TokenCache() self._client = self._get_auth_client(tenant_id, client_id, cache=cache, **kwargs) self._client_id = client_id
def test_interactive_credential_timeout(): # mock transport handles MSAL's tenant discovery transport = Mock(send=lambda _, **__: mock_response( json_payload={ "authorization_endpoint": "https://a/b", "token_endpoint": "https://a/b" })) # mock local server blocks long enough to exceed the timeout timeout = 0.01 server_instance = Mock( wait_for_redirect=functools.partial(time.sleep, timeout + 0.01)) server_class = Mock(return_value=server_instance) credential = InteractiveBrowserCredential( client_id="guid", _server_class=server_class, timeout=timeout, transport=transport, instance_discovery= False, # kwargs are passed to MSAL; this one prevents an AAD verification request _cache=TokenCache(), ) with pytest.raises(ClientAuthenticationError) as ex: credential.get_token("scope") assert "timed out" in ex.value.message.lower()
def test_timeout(): """get_token should raise ClientAuthenticationError when the server times out without receiving a redirect""" timeout = 0.01 class GuaranteedTimeout(AuthCodeRedirectServer, object): def handle_request(self): time.sleep(timeout + 0.01) super(GuaranteedTimeout, self).handle_request() # mock transport handles MSAL's tenant discovery transport = Mock(send=lambda _, **__: mock_response( json_payload={ "authorization_endpoint": "https://a/b", "token_endpoint": "https://a/b" })) credential = InteractiveBrowserCredential(timeout=timeout, transport=transport, _cache=TokenCache(), _server_class=GuaranteedTimeout) with patch(WEBBROWSER_OPEN, lambda _: True): with pytest.raises(ClientAuthenticationError) as ex: credential.get_token("scope") assert "timed out" in ex.value.message.lower()
def test_policies_configurable(): policy = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock()) transport = validating_transport( requests=[Request()] * 3, responses=[ # expected requests: discover tenant, start device code flow, poll for completion get_discovery_response(), mock_response( json_payload={ "device_code": "_", "user_code": "user-code", "verification_uri": "verification-uri", "expires_in": 42, }), mock_response(json_payload=dict( build_aad_response(access_token="**"), scope="scope")), ], ) credential = DeviceCodeCredential(client_id="client-id", prompt_callback=Mock(), policies=[policy], transport=transport, _cache=TokenCache()) credential.get_token("scope") assert policy.on_request.called
def test_timeout(): transport = validating_transport( requests=[Request()] * 3, # not validating requests because they're formed by MSAL responses=[ # expected requests: discover tenant, start device code flow, poll for completion mock_response( json_payload={ "authorization_endpoint": "https://a/b", "token_endpoint": "https://a/b" }), mock_response(json_payload={ "device_code": "_", "user_code": "_", "verification_uri": "_" }), mock_response(json_payload={"error": "authorization_pending"}), ], ) credential = DeviceCodeCredential( client_id="_", prompt_callback=Mock(), transport=transport, timeout=0.01, instance_discovery=False, _cache=TokenCache(), ) with pytest.raises(ClientAuthenticationError) as ex: credential.get_token("scope") assert "timed out" in ex.value.message.lower()
def test_no_browser(): transport = validating_transport(requests=[Request()] * 2, responses=[get_discovery_response()] * 2) credential = InteractiveBrowserCredential( client_id="client-id", _server_class=Mock(), transport=transport, _cache=TokenCache() ) with pytest.raises(ClientAuthenticationError, match=r".*browser.*"): credential.get_token("scope")
def __init__(self, tenant_id, client_id, client_secret, **kwargs): # type: (str, str, str, **Any) -> None if not client_id: raise ValueError( "client_id should be the id of an Azure Active Directory application" ) if not client_secret: raise ValueError( "secret should be an Azure Active Directory application's client secret" ) if not tenant_id: raise ValueError( "tenant_id should be an Azure Active Directory tenant's id (also called its 'directory id')" ) validate_tenant_id(tenant_id) enable_persistent_cache = kwargs.pop("enable_persistent_cache", False) if enable_persistent_cache: allow_unencrypted = kwargs.pop("allow_unencrypted_cache", False) cache = load_service_principal_cache(allow_unencrypted) else: cache = TokenCache() self._client = self._get_auth_client(tenant_id, client_id, cache=cache, **kwargs) self._client_id = client_id self._secret = client_secret
def test_device_code_credential(): client_id = "client-id" expected_token = "access-token" user_code = "user-code" verification_uri = "verification-uri" expires_in = 42 transport = validating_transport( requests=[Request()] * 3, # not validating requests because they're formed by MSAL responses=[ # expected requests: discover tenant, start device code flow, poll for completion mock_response( json_payload={ "authorization_endpoint": "https://a/b", "token_endpoint": "https://a/b" }), mock_response( json_payload={ "device_code": "_", "user_code": user_code, "verification_uri": verification_uri, "expires_in": expires_in, }), mock_response(json_payload=dict( build_aad_response( access_token=expected_token, expires_in=expires_in, refresh_token="_", id_token=build_id_token(aud=client_id), ), scope="scope", ), ), ], ) callback = Mock() credential = DeviceCodeCredential( client_id=client_id, prompt_callback=callback, transport=transport, instance_discovery=False, _cache=TokenCache(), ) now = datetime.datetime.utcnow() token = credential.get_token("scope") assert token.token == expected_token # prompt_callback should have been called as documented assert callback.call_count == 1 uri, code, expires_on = callback.call_args[0] assert uri == verification_uri assert code == user_code # validating expires_on exactly would require depending on internals of the credential and # patching time, so we'll be satisfied if expires_on is a datetime at least expires_in # seconds later than our call to get_token assert isinstance(expires_on, datetime.datetime) assert expires_on - now >= datetime.timedelta(seconds=expires_in)
def test_policies_configurable(): policy = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock()) client_id = "client-id" transport = validating_transport( requests=[Request()] * 2, responses=[ get_discovery_response(), mock_response(json_payload=build_aad_response( access_token="**", id_token=build_id_token(aud=client_id))), ], ) # mock local server fakes successful authentication by immediately returning a well-formed response oauth_state = "oauth-state" auth_code_response = {"code": "authorization-code", "state": [oauth_state]} server_class = Mock(return_value=Mock( wait_for_redirect=lambda: auth_code_response)) credential = InteractiveBrowserCredential(policies=[policy], client_id=client_id, transport=transport, _server_class=server_class, _cache=TokenCache()) with patch("azure.identity._credentials.browser.uuid.uuid4", lambda: oauth_state): credential.get_token("scope") assert policy.on_request.called
def test_user_agent(): client_id = "client-id" transport = validating_transport( requests=[ Request(), Request(required_headers={"User-Agent": USER_AGENT}) ], responses=[ get_discovery_response(), mock_response(json_payload=build_aad_response( access_token="**", id_token=build_id_token(aud=client_id))), ], ) # mock local server fakes successful authentication by immediately returning a well-formed response oauth_state = "oauth-state" auth_code_response = {"code": "authorization-code", "state": [oauth_state]} server_class = Mock(return_value=Mock( wait_for_redirect=lambda: auth_code_response)) credential = InteractiveBrowserCredential(client_id=client_id, transport=transport, _server_class=server_class, _cache=TokenCache()) with patch("azure.identity._credentials.browser.uuid.uuid4", lambda: oauth_state): credential.get_token("scope")
def test_cache_multiple_clients(cert_path, cert_password): """the credential shouldn't use tokens issued to other service principals""" access_token_a = "token a" access_token_b = "not " + access_token_a transport_a = msal_validating_transport( requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))] ) transport_b = msal_validating_transport( requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))] ) cache = TokenCache() with patch("azure.identity._internal.msal_credentials._load_persistent_cache") as mock_cache_loader: mock_cache_loader.return_value = Mock(wraps=cache) credential_a = CertificateCredential( "tenant", "client-a", cert_path, password=cert_password, transport=transport_a, cache_persistence_options=TokenCachePersistenceOptions(), ) assert mock_cache_loader.call_count == 1, "credential should load the persistent cache" credential_b = CertificateCredential( "tenant", "client-b", cert_path, password=cert_password, transport=transport_b, cache_persistence_options=TokenCachePersistenceOptions(), ) assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" # A caches a token scope = "scope" token_a = credential_a.get_token(scope) assert token_a.token == access_token_a assert transport_a.send.call_count == 3 # two MSAL discovery requests, one token request # B should get a different token for the same scope token_b = credential_b.get_token(scope) assert token_b.token == access_token_b assert transport_b.send.call_count == 3 assert len(cache.find(TokenCache.CredentialType.ACCESS_TOKEN)) == 2
def test_authenticate(): client_id = "client-id" environment = "localhost" issuer = "https://" + environment tenant_id = "some-tenant" authority = issuer + "/" + tenant_id access_token = "***" scope = "scope" # mock AAD response with id token object_id = "object-id" home_tenant = "home-tenant-id" username = "******" id_token = build_id_token(aud=client_id, iss=issuer, object_id=object_id, tenant_id=home_tenant, username=username) auth_response = build_aad_response(uid=object_id, utid=home_tenant, access_token=access_token, refresh_token="**", id_token=id_token) transport = validating_transport( requests=[Request(url_substring=issuer)] * 4, responses=[get_discovery_response(authority)] * 2 # instance and tenant discovery + [ mock_response( # start device code flow json_payload={ "device_code": "_", "user_code": "user-code", "verification_uri": "verification-uri", "expires_in": 42, } ), mock_response(json_payload=dict(auth_response, scope=scope)), # poll for completion ], ) credential = DeviceCodeCredential( client_id, prompt_callback=Mock(), # prevent credential from printing to stdout transport=transport, authority=environment, tenant_id=tenant_id, _cache=TokenCache(), ) record = credential.authenticate(scopes=(scope, )) # credential should have a cached access token for the scope used in authenticate token = credential.get_token(scope) assert token.token == access_token assert record.authority == environment assert record.home_account_id == object_id + "." + home_tenant assert record.tenant_id == home_tenant assert record.username == username
def test_authority(authority): """the credential should accept an authority, with or without scheme, as an argument or environment variable""" parsed_authority = urlparse(authority) expected_netloc = parsed_authority.netloc or authority # "localhost" parses to netloc "", path "localhost" class MockCredential(SharedTokenCacheCredential): def _get_auth_client(self, authority=None, **kwargs): actual = urlparse(authority) assert actual.scheme == "https" assert actual.netloc == expected_netloc transport = Mock(send=Mock(side_effect=Exception("credential shouldn't send a request"))) MockCredential(_cache=TokenCache(), authority=authority, transport=transport) with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): MockCredential(_cache=TokenCache(), authority=authority, transport=transport)
async def test_empty_cache(): """the credential should raise CredentialUnavailableError when the cache is empty""" with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): await SharedTokenCacheCredential(_cache=TokenCache() ).get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): await SharedTokenCacheCredential( _cache=TokenCache(), username="******").get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): await SharedTokenCacheCredential( _cache=TokenCache(), tenant_id="not-cached").get_token("scope") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): credential = SharedTokenCacheCredential(_cache=TokenCache(), tenant_id="not-cached", username="******") await credential.get_token("scope")
def test_multitenant_cache(): client_id = "client-id" scope = "scope" expected_token = "***" tenant_a = "tenant-a" tenant_b = "tenant-b" tenant_c = "tenant-c" authority = "https://localhost/" + tenant_a cache = TokenCache() cache.add({ "response": build_aad_response(access_token=expected_token), "client_id": client_id, "scope": [scope], "token_endpoint": "/".join((authority, tenant_a, "oauth2/v2.0/token")), }) common_args = dict(authority=authority, cache=cache, client_id=client_id) client_a = AadClient(tenant_id=tenant_a, **common_args) client_b = AadClient(tenant_id=tenant_b, **common_args) # A has a cached token token = client_a.get_cached_access_token([scope]) assert token.token == expected_token # which B shouldn't return assert client_b.get_cached_access_token([scope]) is None # but C allows multitenant auth and should therefore return the token from tenant_a when appropriate client_c = AadClient(tenant_id=tenant_c, allow_multitenant_authentication=True, **common_args) assert client_c.get_cached_access_token([scope]) is None token = client_c.get_cached_access_token([scope], tenant_id=tenant_a) assert token.token == expected_token with patch.dict("os.environ", { EnvironmentVariables.AZURE_IDENTITY_ENABLE_LEGACY_TENANT_SELECTION: "true" }, clear=True): assert client_c.get_cached_access_token([scope], tenant_id=tenant_a) is None
async def test_cache_multiple_clients(): """the credential shouldn't use tokens issued to other service principals""" access_token_a = "token a" access_token_b = "not " + access_token_a transport_a = async_validating_transport( requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))] ) transport_b = async_validating_transport( requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))] ) cache = TokenCache() with patch(ClientSecretCredential.__module__ + "._load_persistent_cache") as mock_cache_loader: mock_cache_loader.return_value = Mock(wraps=cache) credential_a = ClientSecretCredential( "tenant", "client-a", "secret", transport=transport_a, cache_persistence_options=TokenCachePersistenceOptions(), ) assert mock_cache_loader.call_count == 1, "credential should load the persistent cache" credential_b = ClientSecretCredential( "tenant", "client-b", "secret", transport=transport_b, cache_persistence_options=TokenCachePersistenceOptions(), ) assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" # A caches a token scope = "scope" token_a = await credential_a.get_token(scope) assert token_a.token == access_token_a assert transport_a.send.call_count == 1 # B should get a different token for the same scope token_b = await credential_b.get_token(scope) assert token_b.token == access_token_b assert transport_b.send.call_count == 1 assert len(cache.find(TokenCache.CredentialType.ACCESS_TOKEN)) == 2
def __init__( self, client_id="...", request_token=None, cache=None, msal_app_factory=None, transport=None, **kwargs ): self._msal_app_factory = msal_app_factory self._request_token_impl = request_token or Mock() transport = transport or Mock(send=Mock(side_effect=Exception("credential shouldn't send a request"))) super(MockCredential, self).__init__( client_id=client_id, _cache=cache or TokenCache(), transport=transport, **kwargs )
def test_disable_automatic_authentication(): """When configured for strict silent auth, the credential should raise when silent auth fails""" empty_cache = TokenCache() # empty cache makes silent auth impossible transport = Mock(send=Mock(side_effect=Exception("no request should be sent"))) credential = DeviceCodeCredential("client-id", disable_automatic_authentication=True, transport=transport, _cache=empty_cache) with pytest.raises(AuthenticationRequiredError): credential.get_token("scope")
async def test_evicts_invalid_refresh_token(): """when AAD rejects a refresh token, the client should evict that token from its cache""" tenant_id = "tenant-id" client_id = "client-id" invalid_token = "invalid-refresh-token" cache = TokenCache() cache.add({ "response": build_aad_response(uid="id1", utid="tid1", access_token="*", refresh_token=invalid_token) }) cache.add({ "response": build_aad_response(uid="id2", utid="tid2", access_token="*", refresh_token="...") }) assert len(cache.find(TokenCache.CredentialType.REFRESH_TOKEN)) == 2 assert len( cache.find(TokenCache.CredentialType.REFRESH_TOKEN, query={"secret": invalid_token})) == 1 async def send(request, **_): assert request.data["refresh_token"] == invalid_token return mock_response(json_payload={"error": "invalid_grant"}, status_code=400) transport = Mock(send=Mock(wraps=send)) client = AadClient(tenant_id, client_id, transport=transport, cache=cache) with pytest.raises(ClientAuthenticationError): await client.obtain_token_by_refresh_token(scopes=("scope", ), refresh_token=invalid_token) assert transport.send.call_count == 1 assert len(cache.find(TokenCache.CredentialType.REFRESH_TOKEN)) == 1 assert len( cache.find(TokenCache.CredentialType.REFRESH_TOKEN, query={"secret": invalid_token})) == 0
def test_authentication_record_empty_cache(): record = AuthenticationRecord("tenant_id", "client_id", "authority", "home_account_id", "username") transport = Mock( side_effect=Exception("the credential shouldn't send a request")) credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, _cache=TokenCache()) with pytest.raises(CredentialUnavailableError): credential.get_token("scope")
def __init__(self, request_factory, client_id=None, resource_id=None, identity_config=None, **kwargs): # type: (Callable[[str, dict], HttpRequest], Optional[str], Optional[str], Optional[Dict], **Any) -> None self._cache = kwargs.pop("_cache", None) or TokenCache() self._content_callback = kwargs.pop("_content_callback", None) self._identity_config = identity_config or {} if client_id: self._identity_config["client_id"] = client_id if resource_id: self._identity_config["mi_res_id"] = resource_id self._pipeline = self._build_pipeline(**kwargs) self._request_factory = request_factory
def __init__( self, tenant_id, client_id, authority=None, cache=None, **kwargs ): # type: (str, str, Optional[str], Optional[TokenCache], **Any) -> None self._authority = normalize_authority(authority) if authority else get_default_authority() self._tenant_id = tenant_id self._cache = cache or TokenCache() self._client_id = client_id self._pipeline = self._build_pipeline(**kwargs)
def test_close(): transport = MagicMock() credential = SharedTokenCacheCredential(transport=transport, _cache=TokenCache()) with pytest.raises(CredentialUnavailableError): credential.get_token('scope') assert not transport.__enter__.called assert not transport.__exit__.called credential.close() assert not transport.__enter__.called assert transport.__exit__.call_count == 1
def test_disable_automatic_authentication(): """When configured for strict silent auth, the credential should raise when silent auth fails""" empty_cache = TokenCache() # empty cache makes silent auth impossible transport = Mock(send=Mock(side_effect=Exception("no request should be sent"))) credential = InteractiveBrowserCredential( _disable_automatic_authentication=True, transport=transport, _cache=empty_cache ) with patch(WEBBROWSER_OPEN, Mock(side_effect=Exception("credential shouldn't try interactive authentication"))): with pytest.raises(AuthenticationRequiredError): credential.get_token("scope")
def test_access_token_caching(): """'get_token' shouldn't return other users' access tokens""" scope = "scope" forbidden_access_token = "don't use me" expected_access_token = "access token" my_refresh_token = "my refresh token" your_refresh_token = "your refresh token" me = "me" uid = "uidme" utid = "utidme" cache = TokenCache() cache.add( get_account_event( username=me, uid=uid, utid=utid, refresh_token=my_refresh_token, access_token=forbidden_access_token, scopes=[scope], )) you = "you" uid = "uidyou" utid = "utidyou" cache.add( get_account_event( username=you, uid=uid, utid=utid, refresh_token=your_refresh_token, access_token=expected_access_token, scopes=[scope], ))
def __init__(self, tenant_id, client_id, cache=None, **kwargs): # type: (str, str, **Any) -> None authority = kwargs.pop("authority", KnownAuthorities.AZURE_PUBLIC_CLOUD) if authority[-1] == "/": authority = authority[:-1] token_endpoint = "https://" + "/".join((authority, tenant_id, "oauth2/v2.0/token")) config = {"token_endpoint": token_endpoint} self._cache = cache or TokenCache() self._client = Client(server_configuration=config, client_id=client_id) self._client.session.close() self._client.session = self._get_client_session(**kwargs)
def __init__(self, tenant_id, client_id, cache=None, **kwargs): # type: (str, str, Optional[TokenCache], **Any) -> None authority = kwargs.pop("authority", None) authority = normalize_authority(authority) if authority else get_default_authority() token_endpoint = "/".join((authority, tenant_id, "oauth2/v2.0/token")) config = {"token_endpoint": token_endpoint} self._cache = cache or TokenCache() self._client = Client(server_configuration=config, client_id=client_id) self._client.session.close() self._client.session = self._get_client_session(**kwargs)