def test_authority(authority): """the credential should accept an authority, with or without scheme, as an argument or environment variable""" tenant_id = "expected-tenant" parsed_authority = urlparse(authority) expected_netloc = parsed_authority.netloc or authority expected_authority = "https://{}/{}".format(expected_netloc, tenant_id) mock_ctor = Mock( return_value=Mock(acquire_token_silent_with_error=lambda *_, **__: {"access_token": "**", "expires_in": 42}) ) credential = CertificateCredential(tenant_id, "client-id", CERT_PATH, authority=authority) with patch("msal.ConfidentialClientApplication", mock_ctor): # must call get_token because the credential constructs the MSAL application lazily credential.get_token("scope") assert mock_ctor.call_count == 1 _, kwargs = mock_ctor.call_args assert kwargs["authority"] == expected_authority mock_ctor.reset_mock() # authority can be configured via environment variable with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): credential = CertificateCredential(tenant_id, "client-id", CERT_PATH, authority=authority) with patch("msal.ConfidentialClientApplication", mock_ctor): credential.get_token("scope") assert mock_ctor.call_count == 1 _, kwargs = mock_ctor.call_args assert kwargs["authority"] == expected_authority
def test_regional_authority(): """the credential should configure MSAL with a regional authority specified via kwarg or environment variable""" mock_confidential_client = Mock( return_value=Mock(acquire_token_silent_with_error=lambda *_, **__: {"access_token": "**", "expires_in": 3600}), ) for region in RegionalAuthority: mock_confidential_client.reset_mock() with patch.dict("os.environ", {}, clear=True): credential = CertificateCredential("tenant", "client-id", CERT_PATH, regional_authority=region) with patch("msal.ConfidentialClientApplication", mock_confidential_client): # must call get_token because the credential constructs the MSAL application lazily credential.get_token("scope") assert mock_confidential_client.call_count == 1 _, kwargs = mock_confidential_client.call_args assert kwargs["azure_region"] == region mock_confidential_client.reset_mock() # region can be configured via environment variable with patch.dict("os.environ", {EnvironmentVariables.AZURE_REGIONAL_AUTHORITY_NAME: region}, clear=True): credential = CertificateCredential("tenant", "client-id", CERT_PATH) with patch("msal.ConfidentialClientApplication", mock_confidential_client): credential.get_token("scope") assert mock_confidential_client.call_count == 1 _, kwargs = mock_confidential_client.call_args assert kwargs["azure_region"] == region
def test_request_url(cert_path, cert_password, authority): """the credential should accept an authority, with or without scheme, as an argument or environment variable""" tenant_id = "expected_tenant" access_token = "***" 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": access_token}) cred = CertificateCredential( tenant_id, "client-id", cert_path, password=cert_password, transport=Mock(send=mock_send), authority=authority ) token = cred.get_token("scope") assert token.token == access_token # authority can be configured via environment variable with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): credential = CertificateCredential( tenant_id, "client-id", cert_path, password=cert_password, transport=Mock(send=mock_send) ) credential.get_token("scope") assert token.token == access_token
def test_multitenant_authentication_backcompat(cert_path, cert_password): expected_tenant = "expected-tenant" expected_token = "***" def send(request, **_): parsed = urlparse(request.url) if "/oauth2/v2.0/token" not in parsed.path: return get_discovery_response("https://{}/{}".format( parsed.netloc, expected_tenant)) tenant = parsed.path.split("/")[1] token = expected_token if tenant == expected_tenant else expected_token * 2 return mock_response(json_payload=build_aad_response( access_token=token)) credential = CertificateCredential(expected_tenant, "client-id", cert_path, password=cert_password, transport=Mock(send=send)) token = credential.get_token("scope") assert token.token == expected_token # explicitly specifying the configured tenant is okay token = credential.get_token("scope", tenant_id=expected_tenant) assert token.token == expected_token token = credential.get_token("scope", tenant_id="un" + expected_tenant) assert token.token == expected_token
def test_persistent_cache_multiple_clients(cert_path, cert_password): """the credential shouldn't use tokens issued to other service principals""" access_token_a = "token a" access_token_b = "not " + access_token_a transport_a = validating_transport( requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_a))] ) transport_b = validating_transport( requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token=access_token_b))] ) cache = TokenCache() with patch("azure.identity._internal.persistent_cache._load_persistent_cache") as mock_cache_loader: mock_cache_loader.return_value = Mock(wraps=cache) credential_a = CertificateCredential( "tenant", "client-a", cert_path, password=cert_password, enable_persistent_cache=True, transport=transport_a ) assert mock_cache_loader.call_count == 1, "credential should load the persistent cache" credential_b = CertificateCredential( "tenant", "client-b", cert_path, password=cert_password, enable_persistent_cache=True, transport=transport_b ) assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" # A caches a token scope = "scope" token_a = credential_a.get_token(scope) assert token_a.token == access_token_a assert transport_a.send.call_count == 1 # B should get a different token for the same scope token_b = credential_b.get_token(scope) assert token_b.token == access_token_b assert transport_b.send.call_count == 1
def test_user_agent(): transport = msal_validating_transport( requests=[Request(required_headers={"User-Agent": USER_AGENT})], responses=[mock_response(json_payload=build_aad_response(access_token="**"))], ) credential = CertificateCredential("tenant-id", "client-id", CERT_PATH, transport=transport) credential.get_token("scope")
def test_persistent_cache_multiple_clients(cert_path, cert_password): """the credential shouldn't use tokens issued to other service principals""" access_token_a = "token a" access_token_b = "not " + access_token_a transport_a = msal_validating_transport( requests=[Request()], responses=[ mock_response(json_payload=build_aad_response( access_token=access_token_a)) ]) transport_b = msal_validating_transport( requests=[Request()], responses=[ mock_response(json_payload=build_aad_response( access_token=access_token_b)) ]) cache = TokenCache() with patch( "azure.identity._internal.msal_credentials._load_persistent_cache" ) as mock_cache_loader: mock_cache_loader.return_value = Mock(wraps=cache) credential_a = CertificateCredential( "tenant", "client-a", cert_path, password=cert_password, transport=transport_a, cache_persistence_options=TokenCachePersistenceOptions(), ) assert mock_cache_loader.call_count == 1, "credential should load the persistent cache" credential_b = CertificateCredential( "tenant", "client-b", cert_path, password=cert_password, transport=transport_b, cache_persistence_options=TokenCachePersistenceOptions(), ) assert mock_cache_loader.call_count == 2, "credential should load the persistent cache" # A caches a token scope = "scope" token_a = credential_a.get_token(scope) assert token_a.token == access_token_a assert transport_a.send.call_count == 3 # two MSAL discovery requests, one token request # B should get a different token for the same scope token_b = credential_b.get_token(scope) assert token_b.token == access_token_b assert transport_b.send.call_count == 3 assert len(cache.find(TokenCache.CredentialType.ACCESS_TOKEN)) == 2
def test_policies_configurable(): policy = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock()) def send(*_, **__): return mock_response(json_payload=build_aad_response(access_token="**")) credential = CertificateCredential( "tenant-id", "client-id", CERT_PATH, policies=[ContentDecodePolicy(), policy], transport=Mock(send=send) ) credential.get_token("scope") assert policy.on_request.called
def test_request_body(cert_path, cert_password, send_certificate_chain): access_token = "***" authority = "authority.com" client_id = "client-id" expected_scope = "scope" tenant_id = "tenant" def mock_send(request, **kwargs): if not request.body: return get_discovery_response() assert request.body["grant_type"] == "client_credentials" assert request.body["scope"] == expected_scope with open(cert_path, "rb") as cert_file: validate_jwt(request, client_id, cert_file.read(), cert_password, expect_x5c=send_certificate_chain) return mock_response(json_payload=build_aad_response( access_token=access_token)) cred = CertificateCredential( tenant_id, client_id, cert_path, password=cert_password, transport=Mock(send=mock_send), authority=authority, send_certificate_chain=send_certificate_chain, ) token = cred.get_token(expected_scope) assert token.token == access_token # credential should also accept the certificate as bytes with open(cert_path, "rb") as f: cert_bytes = f.read() cred = CertificateCredential( tenant_id, client_id, certificate_data=cert_bytes, password=cert_password, transport=Mock(send=mock_send), authority=authority, send_certificate_chain=send_certificate_chain, ) token = cred.get_token(expected_scope) assert token.token == access_token
def test_policies_configurable(): policy = Mock(spec_set=SansIOHTTPPolicy, on_request=Mock()) transport = msal_validating_transport( requests=[Request()], responses=[mock_response(json_payload=build_aad_response(access_token="**"))] ) credential = CertificateCredential( "tenant-id", "client-id", CERT_PATH, policies=[ContentDecodePolicy(), policy], transport=transport ) credential.get_token("scope") assert policy.on_request.called
def test_request_body(cert_path, cert_password): access_token = "***" authority = "authority.com" client_id = "client-id" expected_scope = "scope" tenant_id = "tenant" def mock_send(request, **kwargs): assert request.body["grant_type"] == "client_credentials" assert request.body["scope"] == expected_scope with open(cert_path, "rb") as cert_file: validate_jwt(request, client_id, cert_file.read()) return mock_response(json_payload={ "token_type": "Bearer", "expires_in": 42, "access_token": access_token }) cred = CertificateCredential(tenant_id, client_id, cert_path, password=cert_password, transport=Mock(send=mock_send), authority=authority) token = cred.get_token(expected_scope) assert token.token == access_token
def test_request_url(cert_path, cert_password): authority = "authority.com" tenant_id = "expected_tenant" access_token = "***" def validate_url(url): parsed = urlparse(url) assert parsed.scheme == "https" assert parsed.netloc == authority assert parsed.path.startswith("/" + tenant_id) def mock_send(request, **kwargs): validate_url(request.url) return mock_response(json_payload={ "token_type": "Bearer", "expires_in": 42, "access_token": access_token }) cred = CertificateCredential(tenant_id, "client-id", cert_path, password=cert_password, transport=Mock(send=mock_send), authority=authority) token = cred.get_token("scope") assert token.token == access_token
def test_request_body(): access_token = "***" authority = "authority.com" tenant_id = "tenant" def validate_url(url): scheme, netloc, path, _, _, _ = urlparse(url) assert scheme == "https" assert netloc == authority assert path.startswith("/" + tenant_id) def mock_send(request, **kwargs): jwt = request.body["client_assertion"] header, payload, signature = (urlsafeb64_decode(s) for s in jwt.split(".")) claims = json.loads(payload.decode("utf-8")) validate_url(claims["aud"]) return mock_response(json_payload={ "token_type": "Bearer", "expires_in": 42, "access_token": access_token }) cred = CertificateCredential(tenant_id, "client_id", CERT_PATH, transport=Mock(send=mock_send), authority=authority) token = cred.get_token("scope") assert token.token == access_token
def test_certificate_credential(live_certificate_settings): credential = CertificateCredential( live_certificate_settings["client_id"], live_certificate_settings["tenant_id"], live_certificate_settings["cert_path"], ) token = credential.get_token(ARM_SCOPE) assert token assert token.token assert token.expires_on
def test_request_url(cert_path, cert_password): authority = "authority.com" tenant_id = "expected_tenant" access_token = "***" def validate_url(url): parsed = urlparse(url) assert parsed.scheme == "https" assert parsed.netloc == authority assert parsed.path.startswith("/" + tenant_id) def mock_send(request, **kwargs): validate_url(request.url) return mock_response(json_payload={ "token_type": "Bearer", "expires_in": 42, "access_token": access_token }) cred = CertificateCredential(tenant_id, "client-id", cert_path, password=cert_password, transport=Mock(send=mock_send), authority=authority) token = cred.get_token("scope") assert token.token == access_token # authority can be configured via environment variable with patch.dict("os.environ", {EnvironmentVariables.AZURE_AUTHORITY_HOST: authority}, clear=True): credential = CertificateCredential(tenant_id, "client-id", cert_path, password=cert_password, transport=Mock(send=mock_send)) credential.get_token("scope") assert token.token == access_token
def test_multitenant_authentication(cert_path, cert_password): first_tenant = "first-tenant" first_token = "***" second_tenant = "second-tenant" second_token = first_token * 2 def send(request, **_): parsed = urlparse(request.url) tenant = parsed.path.split("/")[1] assert tenant in (first_tenant, second_tenant, "common"), 'unexpected tenant "{}"'.format(tenant) if "/oauth2/v2.0/token" not in parsed.path: return get_discovery_response("https://{}/{}".format( parsed.netloc, tenant)) token = first_token if tenant == first_tenant else second_token return mock_response(json_payload=build_aad_response( access_token=token)) credential = CertificateCredential( first_tenant, "client-id", cert_path, password=cert_password, transport=Mock(send=send), ) token = credential.get_token("scope") assert token.token == first_token token = credential.get_token("scope", tenant_id=first_tenant) assert token.token == first_token token = credential.get_token("scope", tenant_id=second_tenant) assert token.token == second_token # should still default to the first tenant token = credential.get_token("scope") assert token.token == first_token
def test_multitenant_authentication_backcompat(cert_path, cert_password): """When allow_multitenant_authentication is True, the credential should respect get_token(tenant_id=...)""" expected_tenant = "expected-tenant" expected_token = "***" def send(request, **_): parsed = urlparse(request.url) if "/oauth2/v2.0/token" not in parsed.path: return get_discovery_response("https://{}/{}".format(parsed.netloc, expected_tenant)) tenant = parsed.path.split("/")[1] token = expected_token if tenant == expected_tenant else expected_token * 2 return mock_response(json_payload=build_aad_response(access_token=token)) credential = CertificateCredential( expected_tenant, "client-id", cert_path, password=cert_password, transport=Mock(send=send) ) token = credential.get_token("scope") assert token.token == expected_token # explicitly specifying the configured tenant is okay token = credential.get_token("scope", tenant_id=expected_tenant) assert token.token == expected_token # but any other tenant should get an error with pytest.raises(ClientAuthenticationError, match="allow_multitenant_authentication"): credential.get_token("scope", tenant_id="un" + expected_tenant) # ...unless the compat switch is enabled with patch.dict( os.environ, {EnvironmentVariables.AZURE_IDENTITY_ENABLE_LEGACY_TENANT_SELECTION: "true"}, clear=True ): token = credential.get_token("scope", tenant_id="un" + expected_tenant) assert token.token == expected_token, "credential should ignore tenant_id kwarg when the compat switch is enabled"
def test_no_scopes(): """The credential should raise ValueError when get_token is called with no scopes""" credential = CertificateCredential("tenant-id", "client-id", CERT_PATH) with pytest.raises(ValueError): credential.get_token()