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_certificate_credential(live_certificate): tenant_id = live_certificate["tenant_id"] client_id = live_certificate["client_id"] credential = CertificateCredential(tenant_id, client_id, live_certificate["cert_path"]) get_token(credential) credential = CertificateCredential( tenant_id, client_id, live_certificate["cert_with_password_path"], password=live_certificate["password"]) get_token(credential) credential = CertificateCredential( tenant_id, client_id, certificate_data=live_certificate["cert_bytes"]) get_token(credential) credential = CertificateCredential( tenant_id, client_id, certificate_data=live_certificate["cert_with_password_bytes"], password=live_certificate["password"], ) get_token(credential)
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() # 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", PEM_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 = "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_enable_persistent_cache(cert_path, cert_password): """the credential should use the persistent cache only when given enable_persistent_cache=True""" persistent_cache = "azure.identity._internal.persistent_cache" required_arguments = ("tenant-id", "client-id", cert_path) # credential should default to an in memory cache raise_when_called = Mock(side_effect=Exception("credential shouldn't attempt to load a persistent cache")) with patch(persistent_cache + "._load_persistent_cache", raise_when_called): CertificateCredential(*required_arguments, password=cert_password) # allowing an unencrypted cache doesn't count as opting in to the persistent cache CertificateCredential(*required_arguments, password=cert_password, allow_unencrypted_cache=True) # keyword argument opts in to persistent cache with patch(persistent_cache + ".msal_extensions") as mock_extensions: CertificateCredential(*required_arguments, password=cert_password, enable_persistent_cache=True) assert mock_extensions.PersistedTokenCache.call_count == 1 # opting in on an unsupported platform raises an exception with patch(persistent_cache + ".sys.platform", "commodore64"): with pytest.raises(NotImplementedError): CertificateCredential(*required_arguments, password=cert_password, enable_persistent_cache=True) with pytest.raises(NotImplementedError): CertificateCredential( *required_arguments, password=cert_password, enable_persistent_cache=True, allow_unencrypted_cache=True )
def test_non_rsa_key(): """The credential should raise ValueError when given a cert without an RSA private key""" with pytest.raises(ValueError, match=".*RS256.*"): CertificateCredential("tenant-id", "client-id", EC_CERT_PATH) with pytest.raises(ValueError, match=".*RS256.*"): CertificateCredential("tenant-id", "client-id", certificate_data=open(EC_CERT_PATH, "rb").read())
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 get_credentials(self): """ Get credentials """ tenant_id = self.config.get('azure.tenant_id') client_id = self.config.get('azure.client_id') client_secret = self.config.get('azure.client_secret') certificate_path = self.config.get('azure.client_certificate_path') username = self.config.get('azure.username') password = self.config.get('azure.password') if tenant_id and client_id and client_secret: return ClientSecretCredential(tenant_id=tenant_id, client_id=client_id, client_secret=client_secret) if tenant_id and client_id and certificate_path: return CertificateCredential(tenant_id=tenant_id, client_id=client_id, certificate_path=certificate_path) if client_id and username and password: return UsernamePasswordCredential(client_id=client_id, username=username, password=password) return DefaultAzureCredential()
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_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_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_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_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_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