async def test_close(): transport = AsyncMockTransport() credential = SharedTokenCacheCredential( _cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport ) await credential.close() assert transport.__aexit__.call_count == 1
async def exercise_credentials(authority_kwarg, expected_authority=None): expected_authority = expected_authority or authority_kwarg async def send(request, **_): url = urlparse(request.url) assert url.scheme == "https", "Unexpected scheme '{}'".format( url.scheme) assert url.netloc == expected_authority, "Expected authority '{}', actual was '{}'".format( expected_authority, url.netloc) return response # environment credential configured with client secret should respect authority environment = { EnvironmentVariables.AZURE_CLIENT_ID: "client_id", EnvironmentVariables.AZURE_CLIENT_SECRET: "secret", EnvironmentVariables.AZURE_TENANT_ID: "tenant_id", } with patch("os.environ", environment): transport = Mock(send=send) if authority_kwarg: credential = DefaultAzureCredential(authority=authority_kwarg, transport=transport) else: credential = DefaultAzureCredential(transport=transport) access_token, _ = await credential.get_token("scope") assert access_token == expected_access_token # managed identity credential should ignore authority with patch("os.environ", {EnvironmentVariables.MSI_ENDPOINT: "https://some.url"}): transport = Mock(send=asyncio.coroutine(lambda *_, **__: response)) if authority_kwarg: credential = DefaultAzureCredential(authority=authority_kwarg, transport=transport) else: credential = DefaultAzureCredential(transport=transport) access_token, _ = await credential.get_token("scope") assert access_token == expected_access_token # shared cache credential should respect authority upn = os.environ.get(EnvironmentVariables.AZURE_USERNAME, "spam@eggs") # preferring environment values to tenant = os.environ.get(EnvironmentVariables.AZURE_TENANT_ID, "tenant") # prevent failure during live runs account = get_account_event(username=upn, uid="guid", utid=tenant, authority=authority_kwarg) cache = populated_cache(account) with patch.object(SharedTokenCacheCredential, "supported"): credential = DefaultAzureCredential(_cache=cache, authority=authority_kwarg, transport=Mock(send=send)) access_token, _ = await credential.get_token("scope") assert access_token == expected_access_token
async def test_user_agent(): transport = async_validating_transport( requests=[Request(required_headers={"User-Agent": USER_AGENT})], responses=[mock_response(json_payload=build_aad_response(access_token="**"))], ) credential = SharedTokenCacheCredential( _cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport ) await credential.get_token("scope")
async def test_context_manager(): transport = AsyncMockTransport() credential = SharedTokenCacheCredential(_cache=populated_cache( get_account_event("test@user", "uid", "utid")), transport=transport) async with credential: assert transport.__aenter__.call_count == 1 assert transport.__aenter__.call_count == 1 assert transport.__aexit__.call_count == 1
def test_shared_cache_username(): expected_access_token = "expected-access-token" refresh_token_a = "refresh-token-a" refresh_token_b = "refresh-token-b" upn_a = "spam@eggs" upn_b = "eggs@spam" tenant_id = "the-tenant" # two cached accounts, same tenant, different usernames -> shared_cache_username should prevail account_a = get_account_event(username=upn_a, uid="another-guid", utid=tenant_id, refresh_token=refresh_token_a) account_b = get_account_event(username=upn_b, uid="more-guid", utid=tenant_id, refresh_token=refresh_token_b) cache = populated_cache(account_a, account_b) credential = get_credential_for_shared_cache_test( refresh_token_a, expected_access_token, cache, shared_cache_username=upn_a) token = credential.get_token("scope") assert token.token == expected_access_token # redundantly specifying shared_cache_tenant_id makes no difference credential = get_credential_for_shared_cache_test( refresh_token_a, expected_access_token, cache, shared_cache_tenant_id=tenant_id, shared_cache_username=upn_a) token = credential.get_token("scope") assert token.token == expected_access_token # shared_cache_username should prevail over AZURE_USERNAME with patch("os.environ", {EnvironmentVariables.AZURE_USERNAME: upn_b}): credential = get_credential_for_shared_cache_test( refresh_token_a, expected_access_token, cache, shared_cache_username=upn_a) token = credential.get_token("scope") assert token.token == expected_access_token # AZURE_USERNAME should be used when shared_cache_username isn't specified with patch("os.environ", {EnvironmentVariables.AZURE_USERNAME: upn_b}): credential = get_credential_for_shared_cache_test( refresh_token_b, expected_access_token, cache) token = credential.get_token("scope") assert token.token == expected_access_token
async def test_shared_cache_tenant_id(): expected_access_token = "expected-access-token" refresh_token_a = "refresh-token-a" refresh_token_b = "refresh-token-b" upn = "spam@eggs" tenant_a = "tenant-a" tenant_b = "tenant-b" # two cached accounts, same username, different tenants -> shared_cache_tenant_id should prevail account_a = get_account_event(username=upn, uid="another-guid", utid=tenant_a, refresh_token=refresh_token_a) account_b = get_account_event(username=upn, uid="more-guid", utid=tenant_b, refresh_token=refresh_token_b) cache = populated_cache(account_a, account_b) credential = get_credential_for_shared_cache_test( refresh_token_b, expected_access_token, cache, shared_cache_tenant_id=tenant_b) token = await credential.get_token("scope") assert token.token == expected_access_token # redundantly specifying shared_cache_username makes no difference credential = get_credential_for_shared_cache_test( refresh_token_b, expected_access_token, cache, shared_cache_tenant_id=tenant_b, shared_cache_username=upn) token = await credential.get_token("scope") assert token.token == expected_access_token # shared_cache_tenant_id should prevail over AZURE_TENANT_ID with patch("os.environ", {EnvironmentVariables.AZURE_TENANT_ID: tenant_a}): credential = get_credential_for_shared_cache_test( refresh_token_b, expected_access_token, cache, shared_cache_tenant_id=tenant_b) token = await credential.get_token("scope") assert token.token == expected_access_token # AZURE_TENANT_ID should be used when shared_cache_tenant_id isn't specified with patch("os.environ", {EnvironmentVariables.AZURE_TENANT_ID: tenant_b}): credential = get_credential_for_shared_cache_test( refresh_token_b, expected_access_token, cache) token = await credential.get_token("scope") assert token.token == expected_access_token
async def test_authority_aliases(): """the credential should use a refresh token valid for any known alias of its authority""" expected_access_token = "access-token" for authority in KNOWN_ALIASES: # cache a token for this authority expected_refresh_token = authority.replace(".", "") account = get_account_event("spam@eggs", "uid", "tenant", authority=authority, refresh_token=expected_refresh_token) cache = populated_cache(account) # the token should be acceptable for this authority itself transport = async_validating_transport( requests=[ Request( authority=authority, required_data={"refresh_token": expected_refresh_token}) ], responses=[ mock_response(json_payload=build_aad_response( access_token=expected_access_token)) ], ) credential = SharedTokenCacheCredential(authority=authority, _cache=cache, transport=transport) token = await credential.get_token("scope") assert token.token == expected_access_token # it should also be acceptable for every known alias of this authority for alias in KNOWN_ALIASES[authority]: transport = async_validating_transport( requests=[ Request(authority=alias, required_data={ "refresh_token": expected_refresh_token }) ], responses=[ mock_response(json_payload=build_aad_response( access_token=expected_access_token)) ], ) credential = SharedTokenCacheCredential(authority=alias, _cache=cache, transport=transport) token = await credential.get_token("scope") assert token.token == expected_access_token
async def test_no_matching_account_for_tenant(): """one cached account, tenant specified, tenant doesn't match -> credential should raise""" upn = "spam@eggs" tenant = "some-guid" account = get_account_event(username=upn, uid="uid", utid=tenant, refresh_token="refresh-token") cache = populated_cache(account) with pytest.raises(CredentialUnavailableError) as ex: await SharedTokenCacheCredential(_cache=cache, tenant_id="not-" + tenant).get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) assert "not-" + tenant in ex.value.message
async def test_no_matching_account_for_username(): """one cached account, username specified, username doesn't match -> credential should raise""" upn = "spam@eggs" tenant = "some-guid" account = get_account_event(username=upn, uid="uid", utid=tenant, refresh_token="refresh-token") cache = populated_cache(account) with pytest.raises(ClientAuthenticationError) as ex: await SharedTokenCacheCredential(_cache=cache, username="******" + upn).get_token("scope") assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")]) discovered_accounts = ex.value.message.splitlines()[-1] assert upn in discovered_accounts and tenant in discovered_accounts
async def test_no_refresh_token(): """one cached account, account has no refresh token -> credential should raise""" account = get_account_event(uid="uid_a", utid="utid", username="******", refresh_token=None) cache = populated_cache(account) transport = Mock(side_effect=Exception()) # credential shouldn't use the network credential = SharedTokenCacheCredential(_cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): await credential.get_token("scope") credential = SharedTokenCacheCredential(_cache=cache, transport=transport, username="******") with pytest.raises(CredentialUnavailableError, match=NO_ACCOUNTS): await credential.get_token("scope")
async def test_authority_with_no_known_alias(): """given an appropriate token, an authority with no known aliases should work""" authority = "unknown.authority" expected_access_token = "access-token" expected_refresh_token = "refresh-token" account = get_account_event("spam@eggs", "uid", "tenant", authority=authority, refresh_token=expected_refresh_token) cache = populated_cache(account) transport = async_validating_transport( requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) credential = SharedTokenCacheCredential(authority=authority, _cache=cache, transport=transport) token = await credential.get_token("scope") assert token.token == expected_access_token
async def test_policies_configurable(): policy = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock()) async def send(*_, **__): return mock_response(json_payload=build_aad_response(access_token="**")) credential = SharedTokenCacheCredential( _cache=populated_cache(get_account_event("test@user", "uid", "utid")), policies=[policy], transport=Mock(send=send), ) await credential.get_token("scope") assert policy.on_request.called
async def test_close(): async def send(*_, **__): return mock_response(json_payload=build_aad_response(access_token="**")) transport = AsyncMockTransport(send=send) credential = SharedTokenCacheCredential( _cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport ) # the credential doesn't open a transport session before one is needed, so we send a request await credential.get_token("scope") await credential.close() assert transport.__aexit__.call_count == 1
async def test_shared_cache_username(): expected_access_token = "expected-access-token" refresh_token_a = "refresh-token-a" refresh_token_b = "refresh-token-b" upn_a = "spam@eggs" upn_b = "eggs@spam" # The value of tenant_id is arbitrary because this test verifies the credential's behavior given a specified # username. During a complete live test run, $AZURE_TENANT_ID will have a value which DefaultAzureCredential # should pass to SharedTokenCacheCredential. This test will fail if the mock accounts don't match that value. tenant_id = os.environ.get(EnvironmentVariables.AZURE_TENANT_ID, "the-tenant") # two cached accounts, same tenant, different usernames -> shared_cache_username should prevail account_a = get_account_event(username=upn_a, uid="another-guid", utid=tenant_id, refresh_token=refresh_token_a) account_b = get_account_event(username=upn_b, uid="more-guid", utid=tenant_id, refresh_token=refresh_token_b) cache = populated_cache(account_a, account_b) credential = get_credential_for_shared_cache_test( refresh_token_a, expected_access_token, cache, shared_cache_username=upn_a) token = await credential.get_token("scope") assert token.token == expected_access_token # shared_cache_username should prevail over AZURE_USERNAME with patch("os.environ", {EnvironmentVariables.AZURE_USERNAME: upn_b}): credential = get_credential_for_shared_cache_test( refresh_token_a, expected_access_token, cache, shared_cache_username=upn_a) token = await credential.get_token("scope") assert token.token == expected_access_token # AZURE_USERNAME should be used when shared_cache_username isn't specified with patch("os.environ", {EnvironmentVariables.AZURE_USERNAME: upn_b}): credential = get_credential_for_shared_cache_test( refresh_token_b, expected_access_token, cache) token = await credential.get_token("scope") assert token.token == expected_access_token
async def test_two_accounts_no_username_or_tenant(): """two cached accounts, no username or tenant specified -> credential should raise""" upn_a = "a@foo" upn_b = "b@foo" account_a = get_account_event(username=upn_a, uid="uid_a", utid="utid") account_b = get_account_event(username=upn_b, uid="uid_b", utid="utid") cache = populated_cache(account_a, account_b) # credential can't select an identity => it shouldn't use the network transport = Mock(side_effect=Exception()) # two users in the cache, no username specified -> CredentialUnavailableError credential = SharedTokenCacheCredential(_cache=cache, transport=transport) with pytest.raises(ClientAuthenticationError, match=MULTIPLE_ACCOUNTS) as ex: await credential.get_token("scope")
async def test_authority_environment_variable(): """the credential should accept an authority by environment variable when none is otherwise specified""" authority = "localhost" expected_access_token = "access-token" expected_refresh_token = "refresh-token" account = get_account_event("spam@eggs", "uid", "tenant", authority=authority, refresh_token=expected_refresh_token) cache = populated_cache(account) transport = async_validating_transport( requests=[Request(authority=authority, required_data={"refresh_token": expected_refresh_token})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_access_token))], ) with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): credential = SharedTokenCacheCredential(transport=transport, _cache=cache) token = await credential.get_token("scope") assert token.token == expected_access_token
async def test_multitenant_authentication_not_allowed(): """get_token(tenant_id=...) should raise when allow_multitenant_authentication is False (the default)""" default_tenant = "organizations" expected_token = "***" async def send(request, **_): parsed = urlparse(request.url) tenant_id = parsed.path.split("/")[1] assert tenant_id == default_tenant return mock_response(json_payload=build_aad_response( access_token=expected_token, id_token_claims=id_token_claims(aud="...", iss="...", sub="..."), )) authority = "localhost" expected_account = get_account_event("user", "object-id", "tenant-id", authority=authority, client_id="client-id", refresh_token="**") cache = populated_cache(expected_account) credential = SharedTokenCacheCredential(authority=authority, transport=Mock(send=send), _cache=cache) token = await credential.get_token("scope") assert token.token == expected_token # explicitly specifying the configured tenant is okay token = await credential.get_token("scope", tenant_id=default_tenant) assert token.token == expected_token # but any other tenant should get an error with pytest.raises(ClientAuthenticationError, match="allow_multitenant_authentication"): await credential.get_token("scope", tenant_id="some tenant") # ...unless the compat switch is enabled with patch.dict("os.environ", { EnvironmentVariables.AZURE_IDENTITY_ENABLE_LEGACY_TENANT_SELECTION: "true" }): token = await credential.get_token("scope", tenant_id="some tenant") assert token.token == expected_token, "credential should ignore tenant_id kwarg when the compat switch is enabled"
async def test_auth_record_multiple_accounts_for_username(): tenant_id = "tenant-id" client_id = "client-id" authority = "localhost" object_id = "object-id" home_account_id = object_id + "." + tenant_id username = "******" record = AuthenticationRecord(tenant_id, client_id, authority, home_account_id, username) expected_access_token = "****" expected_refresh_token = "**" expected_account = get_account_event(username, object_id, tenant_id, authority=authority, client_id=client_id, refresh_token=expected_refresh_token) cache = populated_cache( expected_account, get_account_event( # this account matches all but the record's tenant username, object_id, "different-" + tenant_id, authority=authority, client_id=client_id, refresh_token="not-" + expected_refresh_token, ), ) transport = async_validating_transport( requests=[ Request(authority=authority, required_data={"refresh_token": expected_refresh_token}) ], responses=[ mock_response(json_payload=build_aad_response( access_token=expected_access_token)) ], ) credential = SharedTokenCacheCredential(_authentication_record=record, transport=transport, _cache=cache) token = await credential.get_token("scope") assert token.token == expected_access_token
async def test_single_account(): """one cached account, no username specified -> credential should auth that account""" refresh_token = "refresh-token" scope = "scope" account = get_account_event(uid="uid_a", utid="utid", username="******", refresh_token=refresh_token) cache = populated_cache(account) expected_token = "***" transport = async_validating_transport( requests=[Request(required_data={"refresh_token": refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) credential = SharedTokenCacheCredential(_cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == expected_token
async def test_no_matching_account_for_tenant_or_username(): """two cached accounts, username and tenant specified, one account matches each -> credential should raise""" refresh_token_a = "refresh-token-a" refresh_token_b = "refresh-token-b" upn_a = "a@foo" upn_b = "b@foo" tenant_a = "tenant-a" tenant_b = "tenant-b" account_a = get_account_event(username=upn_a, uid="uid_a", utid=tenant_a, refresh_token=refresh_token_a) account_b = get_account_event(username=upn_b, uid="uid_b", utid=tenant_b, refresh_token=refresh_token_b) cache = populated_cache(account_a, account_b) transport = Mock( side_effect=Exception()) # credential shouldn't use the network credential = SharedTokenCacheCredential(username=upn_a, tenant_id=tenant_b, _cache=cache, transport=transport) with pytest.raises(ClientAuthenticationError) as ex: await credential.get_token("scope") assert ex.value.message.startswith( NO_MATCHING_ACCOUNTS[:NO_MATCHING_ACCOUNTS.index("{")]) discovered_accounts = ex.value.message.splitlines()[-1] assert all(s in discovered_accounts for s in (upn_a, upn_b, tenant_a, tenant_b)) credential = SharedTokenCacheCredential(username=upn_b, tenant_id=tenant_a, _cache=cache, transport=transport) with pytest.raises(ClientAuthenticationError) as ex: await credential.get_token("scope") assert ex.value.message.startswith( NO_MATCHING_ACCOUNTS[:NO_MATCHING_ACCOUNTS.index("{")]) discovered_accounts = ex.value.message.splitlines()[-1] assert all(s in discovered_accounts for s in (upn_a, upn_b, tenant_a, tenant_b))
def test_shared_cache_tenant_id(): expected_access_token = "expected-access-token" refresh_token_a = "refresh-token-a" refresh_token_b = "refresh-token-b" # The value of the UPN is arbitrary because this test verifies the credential's behavior given a specified # tenant ID. During a complete live test run, $AZURE_USERNAME will have a value which DefaultAzureCredential # should pass to SharedTokenCacheCredential. This test will fail if the mock accounts don't match that value. upn = os.environ.get(EnvironmentVariables.AZURE_USERNAME, "spam@eggs") tenant_a = "tenant-a" tenant_b = "tenant-b" # two cached accounts, same username, different tenants -> shared_cache_tenant_id should prevail account_a = get_account_event(username=upn, uid="another-guid", utid=tenant_a, refresh_token=refresh_token_a) account_b = get_account_event(username=upn, uid="more-guid", utid=tenant_b, refresh_token=refresh_token_b) cache = populated_cache(account_a, account_b) credential = get_credential_for_shared_cache_test( refresh_token_b, expected_access_token, cache, shared_cache_tenant_id=tenant_b ) token = credential.get_token("scope") assert token.token == expected_access_token # redundantly specifying shared_cache_username makes no difference credential = get_credential_for_shared_cache_test( refresh_token_b, expected_access_token, cache, shared_cache_tenant_id=tenant_b, shared_cache_username=upn ) token = credential.get_token("scope") assert token.token == expected_access_token # shared_cache_tenant_id should prevail over AZURE_TENANT_ID with patch("os.environ", {EnvironmentVariables.AZURE_TENANT_ID: tenant_a}): credential = get_credential_for_shared_cache_test( refresh_token_b, expected_access_token, cache, shared_cache_tenant_id=tenant_b ) token = credential.get_token("scope") assert token.token == expected_access_token # AZURE_TENANT_ID should be used when shared_cache_tenant_id isn't specified with patch("os.environ", {EnvironmentVariables.AZURE_TENANT_ID: tenant_b}): credential = get_credential_for_shared_cache_test(refresh_token_b, expected_access_token, cache) token = credential.get_token("scope") assert token.token == expected_access_token
async def test_authentication_record_no_match(): tenant_id = "tenant-id" client_id = "client-id" authority = "localhost" object_id = "object-id" home_account_id = object_id + "." + tenant_id username = "******" record = AuthenticationRecord(tenant_id, client_id, authority, home_account_id, username) transport = Mock(side_effect=Exception("the credential shouldn't send a request")) cache = populated_cache( get_account_event( "not-" + username, "not-" + object_id, "different-" + tenant_id, client_id="not-" + client_id, ), ) credential = SharedTokenCacheCredential(authentication_record=record, transport=transport, _cache=cache) with pytest.raises(CredentialUnavailableError): await credential.get_token("scope")
async def test_same_tenant_different_usernames(): """two cached accounts, same tenant, different usernames""" access_token_a = "access-token-a" access_token_b = "access-token-b" refresh_token_a = "refresh-token-a" refresh_token_b = "refresh-token-b" upn_a = "spam@eggs" upn_b = "eggs@spam" tenant_id = "the-tenant" account_a = get_account_event(username=upn_a, uid="another-guid", utid=tenant_id, refresh_token=refresh_token_a) account_b = get_account_event(username=upn_b, uid="more-guid", utid=tenant_id, refresh_token=refresh_token_b) cache = populated_cache(account_a, account_b) # with no username specified the credential can't select an identity transport = Mock(side_effect=Exception()) # (so it shouldn't use the network) credential = SharedTokenCacheCredential(tenant_id=tenant_id, _cache=cache, transport=transport) with pytest.raises(ClientAuthenticationError) as ex: await credential.get_token("scope") # error message should indicate multiple matching accounts, and list discovered accounts assert ex.value.message.startswith(MULTIPLE_MATCHING_ACCOUNTS[: MULTIPLE_MATCHING_ACCOUNTS.index("{")]) discovered_accounts = ex.value.message.splitlines()[-1] assert discovered_accounts.count(tenant_id) == 2 assert upn_a in discovered_accounts and upn_b in discovered_accounts # with a username specified, the credential should auth the matching account scope = "scope" transport = async_validating_transport( requests=[Request(required_data={"refresh_token": refresh_token_b, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) credential = SharedTokenCacheCredential(username=upn_b, _cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == access_token_a transport = async_validating_transport( requests=[Request(required_data={"refresh_token": refresh_token_a, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) credential = SharedTokenCacheCredential(username=upn_a, _cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == access_token_a
async def test_two_accounts_username_specified(): """two cached accounts, username specified, one account matches -> credential should auth that account""" scope = "scope" expected_refresh_token = "refresh-token-a" upn_a = "a@foo" upn_b = "b@foo" account_a = get_account_event(username=upn_a, uid="uid_a", utid="utid", refresh_token=expected_refresh_token) account_b = get_account_event(username=upn_b, uid="uid_b", utid="utid", refresh_token="refresh_token_b") cache = populated_cache(account_a, account_b) expected_token = "***" transport = async_validating_transport( requests=[Request(required_data={"refresh_token": expected_refresh_token, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=expected_token))], ) credential = SharedTokenCacheCredential(username=upn_a, _cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == expected_token
async def test_allow_multitenant_authentication(): """When allow_multitenant_authentication is True, the credential should respect get_token(tenant_id=...)""" first_token = "***" second_tenant = "second-tenant" second_token = first_token * 2 async def send(request, **_): parsed = urlparse(request.url) tenant_id = parsed.path.split("/")[1] return mock_response(json_payload=build_aad_response( access_token=second_token if tenant_id == second_tenant else first_token, id_token_claims=id_token_claims(aud="...", iss="...", sub="..."), )) authority = "localhost" expected_account = get_account_event("user", "object-id", "tenant-id", authority=authority, client_id="client-id", refresh_token="**") cache = populated_cache(expected_account) credential = SharedTokenCacheCredential( allow_multitenant_authentication=True, authority=authority, transport=Mock(send=send), _cache=cache) token = await credential.get_token("scope") assert token.token == first_token token = await credential.get_token("scope", tenant_id="organizations") assert token.token == first_token token = await credential.get_token("scope", tenant_id=second_tenant) assert token.token == second_token # should still default to the first tenant token = await credential.get_token("scope") assert token.token == first_token
async def test_two_accounts_no_username_or_tenant(): """two cached accounts, no username or tenant specified -> credential should raise""" upn_a = "a@foo" upn_b = "b@foo" account_a = get_account_event(username=upn_a, uid="uid_a", utid="utid") account_b = get_account_event(username=upn_b, uid="uid_b", utid="utid") cache = populated_cache(account_a, account_b) # credential can't select an identity => it shouldn't use the network transport = Mock(side_effect=Exception()) # two users in the cache, no username specified -> ClientAuthenticationError credential = SharedTokenCacheCredential(_cache=cache, transport=transport) with pytest.raises(ClientAuthenticationError) as ex: await credential.get_token("scope") # error message should mention multiple accounts and list usernames in the cache assert upn_a in ex.value.message and upn_b in ex.value.message assert ex.value.message.splitlines()[:-1] == MULTIPLE_ACCOUNTS.splitlines()[:-1]
async def test_same_username_different_tenants(): """two cached accounts, same username, different tenants""" access_token_a = "access-token-a" access_token_b = "access-token-b" refresh_token_a = "refresh-token-a" refresh_token_b = "refresh-token-b" upn = "spam@eggs" tenant_a = "tenant-a" tenant_b = "tenant-b" account_a = get_account_event(username=upn, uid="another-guid", utid=tenant_a, refresh_token=refresh_token_a) account_b = get_account_event(username=upn, uid="more-guid", utid=tenant_b, refresh_token=refresh_token_b) cache = populated_cache(account_a, account_b) # with no tenant specified the credential can't select an identity transport = Mock(side_effect=Exception()) # (so it shouldn't use the network) credential = SharedTokenCacheCredential(username=upn, _cache=cache, transport=transport) with pytest.raises(CredentialUnavailableError) as ex: await credential.get_token("scope") assert ex.value.message.startswith(MULTIPLE_MATCHING_ACCOUNTS[: MULTIPLE_MATCHING_ACCOUNTS.index("{")]) assert upn in ex.value.message # with tenant specified, the credential should auth the matching account scope = "scope" transport = async_validating_transport( requests=[Request(required_data={"refresh_token": refresh_token_a, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))], ) credential = SharedTokenCacheCredential(tenant_id=tenant_a, _cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == access_token_a transport = async_validating_transport( requests=[Request(required_data={"refresh_token": refresh_token_b, "scope": scope})], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))], ) credential = SharedTokenCacheCredential(tenant_id=tenant_b, _cache=cache, transport=transport) token = await credential.get_token(scope) assert token.token == access_token_b
async def test_context_manager(): async def send(*_, **__): return mock_response(json_payload=build_aad_response(access_token="**")) transport = AsyncMockTransport(send=send) credential = SharedTokenCacheCredential( _cache=populated_cache(get_account_event("test@user", "uid", "utid")), transport=transport ) # async with before initialization: credential should call aexit but not aenter async with credential: await credential.get_token("scope") assert transport.__aenter__.call_count == 0 assert transport.__aexit__.call_count == 1 # async with after initialization: credential should call aenter and aexit async with credential: await credential.get_token("scope") assert transport.__aenter__.call_count == 1 assert transport.__aexit__.call_count == 2
async def test_multitenant_authentication_not_allowed(): default_tenant = "organizations" expected_token = "***" async def send(request, **_): parsed = urlparse(request.url) tenant_id = parsed.path.split("/")[1] assert tenant_id == default_tenant return mock_response(json_payload=build_aad_response( access_token=expected_token, id_token_claims=id_token_claims(aud="...", iss="...", sub="..."), )) authority = "localhost" expected_account = get_account_event("user", "object-id", "tenant-id", authority=authority, client_id="client-id", refresh_token="**") cache = populated_cache(expected_account) credential = SharedTokenCacheCredential(authority=authority, transport=Mock(send=send), _cache=cache) token = await credential.get_token("scope") assert token.token == expected_token token = await credential.get_token("scope", tenant_id=default_tenant) assert token.token == expected_token with patch.dict( "os.environ", {EnvironmentVariables.AZURE_IDENTITY_DISABLE_MULTITENANTAUTH: "true"}): token = await credential.get_token("scope", tenant_id="some tenant") assert token.token == expected_token