async def test_request_url(authority): tenant_id = "expected-tenant" parsed_authority = urlparse(authority) expected_netloc = parsed_authority.netloc or authority # "localhost" parses to netloc "", path "localhost" def mock_send(request, **kwargs): actual = urlparse(request.url) assert actual.scheme == "https" assert actual.netloc == expected_netloc assert actual.path.startswith("/" + tenant_id) return mock_response(json_payload={ "token_type": "Bearer", "expires_in": 42, "access_token": "*" }) client = AsyncAuthnClient(tenant=tenant_id, transport=Mock(send=wrap_in_future(mock_send)), authority=authority) await client.request_token(("scope", )) # authority can be configured via environment variable with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): client = AsyncAuthnClient( tenant=tenant_id, transport=Mock(send=wrap_in_future(mock_send))) await client.request_token(("scope", ))
async def test_imds_credential_retries(): mock_response = Mock( text=lambda: b"{}", headers={ "content-type": "application/json", "Retry-After": "0" }, content_type="application/json", ) mock_send = Mock(return_value=mock_response) total_retries = ImdsCredential._create_config().retry_policy.total_retries for status_code in (404, 429, 500): mock_send.reset_mock() mock_response.status_code = status_code try: await ImdsCredential( transport=Mock(send=wrap_in_future(mock_send), sleep=wrap_in_future(lambda _: None)) ).get_token("scope") except ClientAuthenticationError: pass # first call was availability probe, second the original request; # credential should have then exhausted retries for each of these status codes assert mock_send.call_count == 2 + total_retries
async def test_request_url(): authority = "authority.com" tenant = "expected_tenant" def mock_send(request, **kwargs): scheme, netloc, path, _, _, _ = urlparse(request.url) assert scheme == "https" assert netloc == authority assert path.startswith("/" + tenant) return mock_response(json_payload={ "token_type": "Bearer", "expires_in": 42, "access_token": "***" }) client = AsyncAuthnClient(tenant=tenant, transport=Mock(send=wrap_in_future(mock_send)), authority=authority) await client.request_token(("scope", )) # authority can be configured via environment variable with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): client = AsyncAuthnClient( tenant=tenant, transport=Mock(send=wrap_in_future(mock_send))) await client.request_token(("scope", ))
async def test_cache(): expired = "this token's expired" now = int(time.time()) expired_on = now - 3600 expired_token = AccessToken(expired, expired_on) token_payload = { "access_token": expired, "expires_in": 0, "ext_expires_in": 0, "expires_on": expired_on, "not_before": now, "token_type": "Bearer", } mock_send = Mock(return_value=mock_response(json_payload=token_payload)) transport = Mock(send=wrap_in_future(mock_send)) scope = "scope" credential = ClientSecretCredential("tenant-id", "client-id", "secret", transport=transport) # get_token initially returns the expired token because the credential # doesn't check whether tokens it receives from the service have expired token = await credential.get_token(scope) assert token == expired_token access_token = "new token" token_payload["access_token"] = access_token token_payload["expires_on"] = now + 3600 valid_token = AccessToken(access_token, now + 3600) # second call should observe the cached token has expired, and request another token = await credential.get_token(scope) assert token == valid_token assert mock_send.call_count == 2
async def test_chain_returns_first_token(): expected_token = Mock() first_credential = Mock(get_token=wrap_in_future(lambda _: expected_token)) second_credential = Mock(get_token=Mock()) aggregate = ChainedTokenCredential(first_credential, second_credential) credential = await aggregate.get_token("scope") assert credential is expected_token assert second_credential.get_token.call_count == 0
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=wrap_in_future(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_auth_code_credential(): client_id = "client id" tenant_id = "tenant" expected_code = "auth code" redirect_uri = "https://foo.bar" expected_token = AccessToken("token", 42) mock_client = Mock(spec=object) obtain_by_auth_code = Mock(return_value=expected_token) mock_client.obtain_token_by_authorization_code = wrap_in_future( obtain_by_auth_code) credential = AuthorizationCodeCredential( client_id=client_id, tenant_id=tenant_id, authorization_code=expected_code, redirect_uri=redirect_uri, client=mock_client, ) # first call should redeem the auth code token = await credential.get_token("scope") assert token is expected_token assert obtain_by_auth_code.call_count == 1 _, kwargs = obtain_by_auth_code.call_args assert kwargs["code"] == expected_code # no auth code -> credential should return cached token mock_client.obtain_token_by_authorization_code = None # raise if credential calls this again mock_client.get_cached_access_token = lambda *_: expected_token token = await credential.get_token("scope") assert token is expected_token # no auth code, no cached token -> credential should use refresh token mock_client.get_cached_access_token = lambda *_: None mock_client.get_cached_refresh_tokens = lambda *_: [ "this is a refresh token" ] mock_client.obtain_token_by_refresh_token = wrap_in_future( lambda *_, **__: expected_token) token = await credential.get_token("scope") assert token is expected_token
async def test_distro(): mock_client = mock.Mock(spec=object) token_by_refresh_token = mock.Mock(return_value=None) mock_client.obtain_token_by_refresh_token = wrap_in_future( token_by_refresh_token) mock_client.get_cached_access_token = mock.Mock(return_value=None) with pytest.raises(CredentialUnavailableError): credential = VSCodeCredential(_client=mock_client) token = await credential.get_token("scope")
async def test_no_obtain_token_if_cached(): expected_token = AccessToken("token", 42) mock_client = mock.Mock(spec=object) token_by_refresh_token = mock.Mock(return_value=expected_token) mock_client.obtain_token_by_refresh_token = wrap_in_future(token_by_refresh_token) mock_client.get_cached_access_token = mock.Mock(return_value="VALUE") with mock.patch(VSCodeCredential.__module__ + ".get_credentials", return_value="VALUE"): credential = VSCodeCredential(_client=mock_client) token = await credential.get_token("scope") assert token_by_refresh_token.call_count == 0
async def test_redeem_token(): expected_token = AccessToken("token", 42) expected_value = "value" mock_client = mock.Mock(spec=object) token_by_refresh_token = mock.Mock(return_value=expected_token) mock_client.obtain_token_by_refresh_token = wrap_in_future(token_by_refresh_token) mock_client.get_cached_access_token = mock.Mock(return_value=None) with mock.patch(GET_REFRESH_TOKEN, return_value=expected_value): credential = get_credential(_client=mock_client) token = await credential.get_token("scope") assert token is expected_token token_by_refresh_token.assert_called_with(("scope",), expected_value)
async def test_cache_refresh_token(): expected_token = AccessToken("token", 42) mock_client = mock.Mock(spec=object) token_by_refresh_token = mock.Mock(return_value=expected_token) mock_client.obtain_token_by_refresh_token = wrap_in_future(token_by_refresh_token) mock_client.get_cached_access_token = mock.Mock(return_value=None) mock_get_credentials = mock.Mock(return_value="VALUE") with mock.patch(VisualStudioCodeCredential.__module__ + ".get_credentials", mock_get_credentials): credential = VisualStudioCodeCredential(_client=mock_client) token = await credential.get_token("scope") assert mock_get_credentials.call_count == 1 token = await credential.get_token("scope") assert mock_get_credentials.call_count == 1
async def test_cache_refresh_token(): expected_token = AccessToken("token", 42) mock_client = mock.Mock(spec=object) token_by_refresh_token = mock.Mock(return_value=expected_token) mock_client.obtain_token_by_refresh_token = wrap_in_future(token_by_refresh_token) mock_client.get_cached_access_token = mock.Mock(return_value=None) mock_get_credentials = mock.Mock(return_value="VALUE") credential = get_credential(_client=mock_client) with mock.patch(GET_REFRESH_TOKEN, mock_get_credentials): await credential.get_token("scope") assert mock_get_credentials.call_count == 1 await credential.get_token("scope") assert mock_get_credentials.call_count == 1
async def test_chain_attempts_all_credentials(): async def raise_authn_error(message="it didn't work"): raise ClientAuthenticationError(message) expected_token = AccessToken("expected_token", 0) credentials = [ Mock(get_token=Mock(wraps=raise_authn_error)), Mock(get_token=Mock(wraps=raise_authn_error)), Mock(get_token=wrap_in_future(lambda _: expected_token)), ] token = await ChainedTokenCredential(*credentials).get_token("scope") assert token is expected_token for credential in credentials[:-1]: assert credential.get_token.call_count == 1
async def test_chain_raises_for_unexpected_error(): """the chain should not continue after an unexpected error (i.e. anything but CredentialUnavailableError)""" async def credential_unavailable(message="it didn't work"): raise CredentialUnavailableError(message) expected_message = "it can't be done" credentials = [ Mock(get_token=Mock(wraps=credential_unavailable)), Mock(get_token=Mock(side_effect=ValueError(expected_message))), Mock(get_token=Mock( wraps=wrap_in_future(lambda _: AccessToken("**", 42)))) ] with pytest.raises(ClientAuthenticationError) as ex: await ChainedTokenCredential(*credentials).get_token("scope") assert expected_message in ex.value.message assert credentials[-1].get_token.call_count == 0
async def test_no_obtain_token_if_cached(): expected_token = AccessToken("token", time.time() + 3600) token_by_refresh_token = mock.Mock(return_value=expected_token) mock_client = mock.Mock( get_cached_access_token=mock.Mock(return_value=expected_token), obtain_token_by_refresh_token=wrap_in_future(token_by_refresh_token), ) credential = get_credential(_client=mock_client) with mock.patch( GET_REFRESH_TOKEN, mock.Mock(side_effect=Exception("credential should not acquire a new token")), ): token = await credential.get_token("scope") assert token_by_refresh_token.call_count == 0 assert token.token == expected_token.token assert token.expires_on == expected_token.expires_on
async def test_request_url(): authority = "authority.com" tenant = "expected_tenant" def mock_send(request, **kwargs): scheme, netloc, path, _, _, _ = urlparse(request.url) assert scheme == "https" assert netloc == authority assert path.startswith("/" + tenant) return mock_response(json_payload={ "token_type": "Bearer", "expires_in": 42, "access_token": "***" }) client = AsyncAuthnClient(tenant=tenant, transport=Mock(send=wrap_in_future(mock_send)), authority=authority) await client.request_token(("scope", ))
async def test_cache(): scope = "https://foo.bar" expired = "this token's expired" now = int(time.time()) token_payload = { "access_token": expired, "refresh_token": "", "expires_in": 0, "expires_on": now - 300, # expired 5 minutes ago "not_before": now, "resource": scope, "token_type": "Bearer", } mock_response = mock.Mock( text=lambda encoding=None: json.dumps(token_payload), headers={"content-type": "application/json"}, status_code=200, content_type="application/json", ) mock_send = mock.Mock(return_value=mock_response) credential = ImdsCredential(transport=mock.Mock( send=wrap_in_future(mock_send))) token = await credential.get_token(scope) assert token.token == expired assert mock_send.call_count == 2 # first request was probing for endpoint availability # calling get_token again should provoke another HTTP request good_for_an_hour = "this token's good for an hour" token_payload["expires_on"] = int(time.time()) + 3600 token_payload["expires_in"] = 3600 token_payload["access_token"] = good_for_an_hour token = await credential.get_token(scope) assert token.token == good_for_an_hour assert mock_send.call_count == 3 # get_token should return the cached token now token = await credential.get_token(scope) assert token.token == good_for_an_hour assert mock_send.call_count == 3