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_client_secret_credential(): client_id = "fake-client-id" secret = "fake-client-secret" tenant_id = "fake-tenant-id" access_token = "***" transport = async_validating_transport( requests=[ Request(url_substring=tenant_id, required_data={ "client_id": client_id, "client_secret": secret }) ], responses=[ mock_response( json_payload={ "token_type": "Bearer", "expires_in": 42, "ext_expires_in": 42, "access_token": access_token, }) ], ) token = await ClientSecretCredential( tenant_id=tenant_id, client_id=client_id, client_secret=secret, transport=transport).get_token("scope") # not validating expires_on because doing so requires monkeypatching time, and this is tested elsewhere assert token.token == access_token
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_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_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_cloud_shell(): """Cloud Shell environment: only MSI_ENDPOINT set""" access_token = "****" expires_on = 42 expected_token = AccessToken(access_token, expires_on) url = "http://localhost:42/token" scope = "scope" transport = async_validating_transport( requests=[ Request(url, method="POST", required_headers={"Metadata": "true"}, required_data={"resource": scope}) ], responses=[ mock_response( json_payload={ "access_token": access_token, "expires_in": 0, "expires_on": expires_on, "not_before": int(time.time()), "resource": scope, "token_type": "Bearer", }) ], ) with mock.patch("os.environ", {EnvironmentVariables.MSI_ENDPOINT: url}): token = await ManagedIdentityCredential(transport=transport ).get_token(scope) assert token == expected_token
async def test_client_secret_environment_credential(monkeypatch): client_id = "fake-client-id" secret = "fake-client-secret" tenant_id = "fake-tenant-id" access_token = "***" transport = async_validating_transport( requests=[ Request(url_substring=tenant_id, required_data={ "client_id": client_id, "client_secret": secret }) ], responses=[ mock_response( json_payload={ "token_type": "Bearer", "expires_in": 42, "ext_expires_in": 42, "access_token": access_token, }) ], ) monkeypatch.setenv(EnvironmentVariables.AZURE_CLIENT_ID, client_id) monkeypatch.setenv(EnvironmentVariables.AZURE_CLIENT_SECRET, secret) monkeypatch.setenv(EnvironmentVariables.AZURE_TENANT_ID, tenant_id) token = await EnvironmentCredential(transport=transport).get_token("scope") # not validating expires_on because doing so requires monkeypatching time, and this is tested elsewhere assert token.token == access_token
def get_credential_for_shared_cache_test(expected_refresh_token, expected_access_token, cache, **kwargs): exclude_other_credentials = { option: True for option in ("exclude_environment_credential", "exclude_managed_identity_credential") } # validating transport will raise if the shared cache credential isn't used, or selects the wrong refresh token transport = async_validating_transport( requests=[ Request(required_data={"refresh_token": expected_refresh_token}) ], responses=[ mock_response(json_payload=build_aad_response( access_token=expected_access_token)) ], ) # this credential uses a mock shared cache, so it works on all platforms with patch.object(SharedTokenCacheCredential, "supported", lambda: True): return DefaultAzureCredential(_cache=cache, transport=transport, **exclude_other_credentials, **kwargs)
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_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 = ClientSecretCredential("tenant-id", "client-id", "client-secret", transport=transport) await credential.get_token("scope")
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 = AuthorizationCodeCredential("tenant-id", "client-id", "auth-code", "http://localhost", transport=transport) await credential.get_token("scope")
async def test_imds_user_assigned_identity(): access_token = "****" expires_on = 42 expected_token = AccessToken(access_token, expires_on) url = Endpoints.IMDS scope = "scope" client_id = "some-guid" transport = async_validating_transport( requests=[ Request( url ), # first request should be availability probe => match only the URL Request( url, method="GET", required_headers={ "Metadata": "true", "User-Agent": USER_AGENT }, required_params={ "api-version": "2018-02-01", "client_id": client_id, "resource": scope }, ), ], responses=[ # probe receives error response mock_response(status_code=400, json_payload={"error": "this is an error message"}), mock_response( json_payload={ "access_token": access_token, "client_id": client_id, "expires_in": 42, "expires_on": expires_on, "ext_expires_in": 42, "not_before": int(time.time()), "resource": scope, "token_type": "Bearer", }), ], ) token = await ManagedIdentityCredential( client_id=client_id, transport=transport).get_token(scope) assert token == expected_token
async def test_app_service_user_assigned_identity(): """App Service environment: MSI_ENDPOINT, MSI_SECRET set""" access_token = "****" expires_on = 42 client_id = "some-guid" expected_token = AccessToken(access_token, expires_on) url = "http://localhost:42/token" secret = "expected-secret" scope = "scope" transport = async_validating_transport( requests=[ Request( url, method="GET", required_headers={ "Metadata": "true", "secret": secret, "User-Agent": USER_AGENT }, required_params={ "api-version": "2017-09-01", "clientid": client_id, "resource": scope }, ) ], responses=[ mock_response( json_payload={ "access_token": access_token, "expires_on": expires_on, "resource": scope, "token_type": "Bearer", }) ], ) with mock.patch( "os.environ", { EnvironmentVariables.MSI_ENDPOINT: url, EnvironmentVariables.MSI_SECRET: secret }): token = await ManagedIdentityCredential( client_id=client_id, transport=transport).get_token(scope) assert token == expected_token
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_policy_updates_cache(): """ It's possible for the challenge returned for a request to change, e.g. when a vault is moved to a new tenant. When the policy receives a 401, it should update the cached challenge for the requested URL, if one exists. """ # ensure the test starts with an empty cache HttpChallengeCache.clear() url = "https://azure.service/path" first_scope = "https://first-scope" first_token = "first-scope-token" second_scope = "https://second-scope" second_token = "second-scope-token" challenge_fmt = 'Bearer authorization="https://login.authority.net/tenant", resource={}' # mocking a tenant change: # 1. first request -> respond with challenge # 2. second request should be authorized according to the challenge -> respond with success # 3. third request should match the second -> respond with a new challenge # 4. fourth request should be authorized according to the new challenge -> respond with success # 5. fifth request should match the fourth -> respond with success transport = async_validating_transport( requests=( Request(url), Request(url, required_headers={ "Authorization": "Bearer {}".format(first_token) }), Request(url, required_headers={ "Authorization": "Bearer {}".format(first_token) }), Request(url, required_headers={ "Authorization": "Bearer {}".format(second_token) }), Request(url, required_headers={ "Authorization": "Bearer {}".format(second_token) }), ), responses=( mock_response(status_code=401, headers={ "WWW-Authenticate": challenge_fmt.format(first_scope) }), mock_response(status_code=200), mock_response(status_code=401, headers={ "WWW-Authenticate": challenge_fmt.format(second_scope) }), mock_response(status_code=200), mock_response(status_code=200), ), ) tokens = (t for t in [first_token] * 2 + [second_token] * 2) credential = Mock( get_token=asyncio.coroutine(lambda _: AccessToken(next(tokens), 0))) pipeline = AsyncPipeline( policies=[AsyncChallengeAuthPolicy(credential=credential)], transport=transport) # policy should complete and cache the first challenge await pipeline.run(HttpRequest("GET", url)) # The next request will receive a challenge. The policy should handle it and update the cache entry. await pipeline.run(HttpRequest("GET", url))