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_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_no_matching_account_for_tenant_and_username():
    """one cached account, tenant and username specified, neither 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, username="******" + upn).get_token(
            "scope"
        )

    assert ex.value.message.startswith(NO_MATCHING_ACCOUNTS[: NO_MATCHING_ACCOUNTS.index("{")])
    assert "not" + upn in ex.value.message and "not-" + tenant in ex.value.message
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_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_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_tenant_specified():
    """two cached accounts, tenant 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"
    tenant_id = "tenant-id"
    account_a = get_account_event(username=upn_a,
                                  uid="uid_a",
                                  utid=tenant_id,
                                  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(tenant_id=tenant_id,
                                            _cache=cache,
                                            transport=transport)
    token = await credential.get_token(scope)
    assert token.token == expected_token
Esempio n. 8
0
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
Esempio n. 9
0
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"
Esempio n. 10
0
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 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
Esempio n. 12
0
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_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
Esempio n. 14
0
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
Esempio n. 15
0
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
async def test_authentication_record():
    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 = "**"
    account = get_account_event(username,
                                object_id,
                                tenant_id,
                                authority=authority,
                                client_id=client_id,
                                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(_authentication_record=record,
                                            transport=transport,
                                            _cache=cache)

    token = await credential.get_token("scope")
    assert token.token == expected_access_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(CredentialUnavailableError) as ex:
        await credential.get_token("scope")

    assert ex.value.message.startswith(
        MULTIPLE_MATCHING_ACCOUNTS[:MULTIPLE_MATCHING_ACCOUNTS.index("{")])
    assert tenant_id in ex.value.message

    # 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_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(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(upn) == 2
    assert tenant_a in discovered_accounts and tenant_b in discovered_accounts

    # 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