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
Exemple #6
0
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
Exemple #7
0
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)
Exemple #9
0
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")
Exemple #10
0
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")
Exemple #11
0
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
Exemple #15
0
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))