Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error 401 using certificate-based authentication (CBA) #1350

Open
darknetehf opened this issue Jan 15, 2025 · 2 comments
Open

Error 401 using certificate-based authentication (CBA) #1350

darknetehf opened this issue Jan 15, 2025 · 2 comments

Comments

@darknetehf
Copy link

darknetehf commented Jan 15, 2025

Hello,

TL;DR: I cannot get exchangelib to work with CBA, although oauth2 works.

I am already using exchangelib with oauth2, but would like to use certificates instead of tokens, because tokens have a limited lifetime.
I have researched related issues: #1183 #1093 #751 and Google.

Working unit test with oauth2:

from exchangelib import IMPERSONATION, OAUTH2, Account, Configuration, OAuth2Credentials
from exchangelib.version import EXCHANGE_O365, Version

CLIENT_ID = "xxx"
CLIENT_SECRET = "xxx"
TENANT_ID = "xxx"
imap_host = "outlook.office365.com"
mailbox_user = "[email protected]"

# authenticate using OAUTH2
credentials = OAuth2Credentials(
    client_id=CLIENT_ID, tenant_id=TENANT_ID, client_secret=CLIENT_SECRET
)
# do not rely on autodiscover but specify explicit server name
config = Configuration(
    server=imap_host, credentials=credentials,
    version=Version(build=EXCHANGE_O365), auth_type=OAUTH2
)


def test_exchange_connection(logger):
    logger.info("Logging in to mailbox")

    # see: https://github.com/ecederstrand/exchangelib/issues/735
    # use IMPERSONATION instead of DELEGATE: "ExchangeImpersonation SOAP header must be present for this type of OAuth token"
    my_account = Account(
        primary_smtp_address=mailbox_user,
        config=config, autodiscover=False, access_type=IMPERSONATION
    )
    logger.info(f"my_account.root.tree(): {my_account.root.tree()}")
    assert my_account.root.child_folder_count > 0

Attempted unit test for CBA:

from exchangelib.protocol import BaseProtocol
from exchangelib import Account, Configuration, DELEGATE, IMPERSONATION, CBA, TLSClientAuth
from exchangelib.version import EXCHANGE_O365, Version

imap_host = "outlook.office365.com"
mailbox_user = "[email protected]"

TLSClientAuth.cert_file = "mycert.pem"
BaseProtocol.HTTP_ADAPTER_CLS = TLSClientAuth

config = Configuration(server=imap_host, auth_type=CBA, version=Version(build=EXCHANGE_O365))

def test_exchange_connection(logger):
    my_account = Account(
        primary_smtp_address=mailbox_user,
        config=config, autodiscover=False,
        access_type=DELEGATE)

    logger.info(f"my_account.root.child_folder_count: {dir(my_account.root.child_folder_count)}")

This results in a 401 error.

Relevant notes:

  • I am not using autodiscover
  • I already use certificates in Azure AD for MSAL with Python and that works
  • The Python code uses a PEM file that combines both private key and public certificate - when using only the private key I had a loop of SSL errors
  • I have tried DELEGATE and IMPERSONATION, not sure if that would make any difference

Is this the correct way or is there anything I should try?
Log below and thanks for looking.

============================= test session starts ==============================
platform linux -- Python 3.12.8, pytest-7.4.4, pluggy-1.5.0
configfile: pytest.ini

----------------------------- live log collection ------------------------------
2025-01-15 18:17:49 - DEBUG - spnego._gss - :55 - Python gssapi not available, cannot use any GSSAPIProxy protocols: No module named 'krb5'
collected 1 item

tests/test_exchange_cba.py::test_exchange_connection 
-------------------------------- live log call ---------------------------------
2025-01-15 18:17:49 - DEBUG - tzlocal - _get_localzone_name:123 - /etc/localtime found
2025-01-15 18:17:49 - DEBUG - tzlocal - _get_localzone_name:139 - 1 found:
 {'/etc/localtime is a symlink to': 'Iceland'}
2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - __call__:408 - Waiting for _protocol_cache_lock
2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - __call__:421 - Protocol __call__ cache miss. Adding key '('https://outlook.office365.com/EWS/Exchange.asmx', None)'
2025-01-15 18:17:49 - DEBUG - exchangelib.account - __init__:206 - Added account: [email protected]
2025-01-15 18:17:49 - INFO - tests.conftest - test_exchange_connection:29 - my_account: ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_consume_item_service', 'access_type', 'ad_response', 'admin_audit_logs', 'affinity_cookie', 'archive_deleted_items', 'archive_inbox', 'archive_msg_folder_root', 'archive_recoverable_items_deletions', 'archive_recoverable_items_purges', 'archive_recoverable_items_root', 'archive_recoverable_items_versions', 'archive_root', 'bulk_archive', 'bulk_copy', 'bulk_create', 'bulk_delete', 'bulk_mark_as_junk', 'bulk_move', 'bulk_send', 'bulk_update', 'calendar', 'conflicts', 'contacts', 'conversation_history', 'create_rule', 'default_timezone', 'delegates', 'delete_rule', 'directory', 'domain', 'drafts', 'export', 'favorites', 'fetch', 'fetch_personas', 'fullname', 'identity', 'im_contact_list', 'inbox', 'journal', 'junk', 'local_failures', 'locale', 'mail_tips', 'msg_folder_root', 'my_contacts', 'notes', 'oof_settings', 'outbox', 'people_connect', 'primary_smtp_address', 'protocol', 'public_folders_root', 'pull_subscription', 'push_subscription', 'quick_contacts', 'recipient_cache', 'recoverable_items_deletions', 'recoverable_items_purges', 'recoverable_items_root', 'recoverable_items_versions', 'root', 'rules', 'search_folders', 'sent', 'server_failures', 'set_rule', 'streaming_subscription', 'subscribe_to_pull', 'subscribe_to_push', 'subscribe_to_streaming', 'sync_issues', 'tasks', 'todo_search', 'trash', 'unsubscribe', 'upload', 'version', 'voice_mail']
2025-01-15 18:17:49 - DEBUG - exchangelib.services.common - _chunked_get_elements:278 - Processing chunk 1 containing 1 items
2025-01-15 18:17:49 - DEBUG - exchangelib.services.common - _get_response_xml:393 - Calling service GetFolder
2025-01-15 18:17:49 - DEBUG - exchangelib.services.common - _get_response_xml:395 - Trying API version Exchange2016
2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - increase_poolsize:206 - Server outlook.office365.com: Increasing session pool size from 0 to 1
2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - create_session:330 - Server outlook.office365.com: Created session 84582
2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - get_session:249 - Server outlook.office365.com: Waiting for session
2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - get_session:255 - Server outlook.office365.com: Got session 84582
2025-01-15 18:17:49 - DEBUG - exchangelib.util - post_ratelimited:811 - Session 84582 thread 140214710770560 timeout 120: POST'ing to https://outlook.office365.com/EWS/Exchange.asmx after 0s sleep
2025-01-15 18:17:49 - DEBUG - urllib3.connectionpool - _new_conn:1049 - Starting new HTTPS connection (1): outlook.office365.com:443
2025-01-15 18:17:50 - DEBUG - urllib3.connectionpool - _make_request:544 - https://outlook.office365.com:443 "POST /EWS/Exchange.asmx HTTP/1.1" 401 0
2025-01-15 18:17:50 - DEBUG - exchangelib.util - post_ratelimited:862 - Timeout: 120
Session: 84582
Thread: 140214710770560
Auth type: None
URL: https://outlook.office365.com/EWS/Exchange.asmx
HTTP adapter: 
Streaming: False
Response time: 0.23992178899788996
Status code: 401
Request headers: {'User-Agent': 'exchangelib/5.5.0 (python-requests/2.32.3)', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Type': 'text/xml; charset=utf-8', 'X-AnchorMailbox': '[email protected]', 'Content-Length': '1214'}
Response headers: {'Content-Length': '0', 'Server': 'Microsoft-HTTPAPI/2.0', 'X-BEServer': 'AS8PR08MB6357', 'X-NanoProxy': '1,1', 'Request-Id': '3d6befc1-e20e-e7b9-1ac1-75aae8bdd77b', 'X-CalculatedFETarget': 'AS9PR07CU001.internal.outlook.com', 'X-BeSku': 'WCS6', 'X-BackEndHttpStatus': '401,401', 'BasicChallengeAdded': 'True', 'MS-CV': 'we9rPQ7iuecawXWq6L3Xew.1.1', 'X-CalculatedBETarget': 'AS8PR08MB6357.eurprd08.prod.outlook.com', 'X-DiagInfo': 'AS8PR08MB6357', 'X-FEEFZInfo': 'DHR', 'X-UserType': 'Business', 'X-FEProxyInfo': 'AS9PR07CA0001', 'X-FEServer': 'DUZPR01CA0137', 'X-Proxy-BackendServerStatus': '401', 'X-Proxy-RoutingCorrectness': '1', 'X-RUM-NotUpdateQueriedPath': '1', 'X-RUM-NotUpdateQueriedDbCopy': '1', 'X-RUM-Validated': '1', 'X-FirstHopCafeEFZ': 'DUB', 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', 'Set-Cookie': 'exchangecookie=a7a24814fef441248023763bc5787ec0; expires=Thu, 15-Jan-2026 18:17:50 GMT; path=/; secure; HttpOnly', 'WWW-Authenticate': 'Basic Realm=""', 'Date': 'Wed, 15 Jan 2025 18:17:49 GMT'}
2025-01-15 18:17:50 - DEBUG - exchangelib.util.xml - post_ratelimited:863 - Request XML: b'\[email protected]'
Response XML: b''
2025-01-15 18:17:50 - DEBUG - exchangelib.protocol - retire_session:279 - Server outlook.office365.com: Retiring session 84582
2025-01-15 18:17:50 - DEBUG - exchangelib.protocol - create_session:330 - Server outlook.office365.com: Created session 79172
2025-01-15 18:17:50 - DEBUG - exchangelib.protocol - release_session:261 - Server outlook.office365.com: Releasing session 79172
FAILED                                                                   [100%]

=================================== FAILURES ===================================
___________________________ test_exchange_connection ___________________________

self = 
obj = 
cls = 

    def __get__(self, obj, cls):
        if obj is None:
            return self
    
        obj_dict = obj.__dict__
        name = self.func.__name__
        with self.lock:
            try:
                # check if the value was computed before the lock was acquired
>               return obj_dict[name]
E               KeyError: 'root'

../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/cached_property.py:63: KeyError

During handling of the above exception, another exception occurred:

logger = 

    def test_exchange_connection(logger):
        my_account = Account(
            primary_smtp_address=mailbox_user,
            config=config, autodiscover=False,
            access_type=DELEGATE)
        logger.info(f"my_account: {dir(my_account)}")
>       logger.info(f"my_account.root.child_folder_count: {dir(my_account.root.child_folder_count)}")

tests/test_exchange_cba.py:30: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/cached_property.py:67: in __get__
    return obj_dict.setdefault(name, self.func(obj))
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/account.py:350: in root
    return Root.get_distinguished(account=self)
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/folders/roots.py:145: in get_distinguished
    return cls._get_distinguished(
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/folders/base.py:226: in _get_distinguished
    return cls.resolve(account=folder.account, folder=folder)
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/folders/base.py:530: in resolve
    folders = list(FolderCollection(account=account, folders=[folder]).resolve())
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/folders/collections.py:335: in resolve
    yield from self.__class__(account=self.account, folders=resolveable_folders).get_folders(
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/folders/collections.py:403: in get_folders
    yield from GetFolder(account=self.account).call(
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/services/get_folder.py:51: in _elems_to_objs
    for folder, elem in zip(self.folders, elems):
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/services/common.py:279: in _chunked_get_elements
    yield from self._get_elements(payload=payload_func(chunk, **kwargs))
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/services/common.py:300: in _get_elements
    yield from self._response_generator(payload=payload)
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/services/common.py:263: in _response_generator
    response = self._get_response_xml(payload=payload)
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/services/common.py:396: in _get_response_xml
    r = self._get_response(payload=payload, api_version=api_version)
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/services/common.py:347: in _get_response
    r, session = post_ratelimited(
../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/util.py:866: in post_ratelimited
    protocol.retry_policy.raise_response_errors(r)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = 
response = 

    def raise_response_errors(self, response):
        if response.status_code == 200:
            # Response is OK
            return
        if response.status_code == 500 and response.content and is_xml(response.content):
            # Some genius at Microsoft thinks it's OK to send a valid SOAP response as an HTTP 500
            log.debug("Got status code %s but trying to parse content anyway", response.status_code)
            return
        cas_error = response.headers.get("X-CasErrorCode")
        if cas_error:
            if cas_error.startswith("CAS error:"):
                # Remove unnecessary text
                cas_error = cas_error.split(":", 1)[1].strip()
            raise CASError(cas_error=cas_error, response=response)
        if response.status_code == 500 and (
            b"The specified server version is invalid" in response.content
            or b"ErrorInvalidSchemaVersionForMailboxVersion" in response.content
        ):
            # Another way of communicating invalid schema versions
            raise ErrorInvalidSchemaVersionForMailboxVersion("Invalid server version")
        if response.headers.get("connection") == "close":
            # Connection closed. OK to retry.
            raise ErrorServerBusy("Caused by closed connection")
        if (
            response.status_code == 302
            and response.headers.get("location", "").lower()
            == "/ews/genericerrorpage.htm?aspxerrorpath=/ews/exchange.asmx"
        ):
            # Redirect to genericerrorpage.htm is ridiculous behaviour for random outages. OK to retry.
            #
            # Redirect to '/internalsite/internalerror.asp' or '/internalsite/initparams.aspx' is caused by e.g. TLS
            # certificate f*ckups on the Exchange server. We should not retry those.
            raise ErrorInternalServerTransientError(f"Caused by HTTP 302 redirect to {response.headers['location']}")
        if response.status_code in (301, 302):
            try:
                redirect_url = get_redirect_url(response=response, allow_relative=False)
            except RelativeRedirect as e:
                log.debug("Redirect not allowed but we were relative redirected (%s -> %s)", response.url, e.value)
                raise RedirectError(url=e.value)
            log.debug("Redirect not allowed but we were redirected ( (%s -> %s)", response.url, redirect_url)
            raise RedirectError(url=redirect_url)
        if b"The referenced account is currently locked out" in response.content:
            raise UnauthorizedError("The referenced account is currently locked out")
        if response.status_code == 401 and self.fail_fast:
            # This is a login failure
>           raise UnauthorizedError(f"Invalid credentials for {response.url}")
E           exchangelib.errors.UnauthorizedError: Invalid credentials for https://outlook.office365.com/EWS/Exchange.asmx

../../.cache/pypoetry/virtualenvs/previsions-yjiFWp2F-py3.12/lib/python3.12/site-packages/exchangelib/protocol.py:727: UnauthorizedError
------------------------------ Captured log call -------------------------------
2025-01-15 18:17:49 - DEBUG - tzlocal - _get_localzone_name:123 - /etc/localtime found
2025-01-15 18:17:49 - DEBUG - tzlocal - _get_localzone_name:139 - 1 found:
 {'/etc/localtime is a symlink to': 'Iceland'}
2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - __call__:408 - Waiting for _protocol_cache_lock
2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - __call__:421 - Protocol __call__ cache miss. Adding key '('https://outlook.office365.com/EWS/Exchange.asmx', None)'
2025-01-15 18:17:49 - DEBUG - exchangelib.account - __init__:206 - Added account: [email protected]
2025-01-15 18:17:49 - INFO - tests.conftest - test_exchange_connection:29 - my_account: ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_consume_item_service', 'access_type', 'ad_response', 'admin_audit_logs', 'affinity_cookie', 'archive_deleted_items', 'archive_inbox', 'archive_msg_folder_root', 'archive_recoverable_items_deletions', 'archive_recoverable_items_purges', 'archive_recoverable_items_root', 'archive_recoverable_items_versions', 'archive_root', 'bulk_archive', 'bulk_copy', 'bulk_create', 'bulk_delete', 'bulk_mark_as_junk', 'bulk_move', 'bulk_send', 'bulk_update', 'calendar', 'conflicts', 'contacts', 'conversation_history', 'create_rule', 'default_timezone', 'delegates', 'delete_rule', 'directory', 'domain', 'drafts', 'export', 'favorites', 'fetch', 'fetch_personas', 'fullname', 'identity', 'im_contact_list', 'inbox', 'journal', 'junk', 'local_failures', 'locale', 'mail_tips', 'msg_folder_root', 'my_contacts', 'notes', 'oof_settings', 'outbox', 'people_connect', 'primary_smtp_address', 'protocol', 'public_folders_root', 'pull_subscription', 'push_subscription', 'quick_contacts', 'recipient_cache', 'recoverable_items_deletions', 'recoverable_items_purges', 'recoverable_items_root', 'recoverable_items_versions', 'root', 'rules', 'search_folders', 'sent', 'server_failures', 'set_rule', 'streaming_subscription', 'subscribe_to_pull', 'subscribe_to_push', 'subscribe_to_streaming', 'sync_issues', 'tasks', 'todo_search', 'trash', 'unsubscribe', 'upload', 'version', 'voice_mail']
2025-01-15 18:17:49 - DEBUG - exchangelib.services.common - _chunked_get_elements:278 - Processing chunk 1 containing 1 items
2025-01-15 18:17:49 - DEBUG - exchangelib.services.common - _get_response_xml:393 - Calling service GetFolder
2025-01-15 18:17:49 - DEBUG - exchangelib.services.common - _get_response_xml:395 - Trying API version Exchange2016
2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - increase_poolsize:206 - Server outlook.office365.com: Increasing session pool size from 0 to 1
2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - create_session:330 - Server outlook.office365.com: Created session 84582
2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - get_session:249 - Server outlook.office365.com: Waiting for session
2025-01-15 18:17:49 - DEBUG - exchangelib.protocol - get_session:255 - Server outlook.office365.com: Got session 84582
2025-01-15 18:17:49 - DEBUG - exchangelib.util - post_ratelimited:811 - Session 84582 thread 140214710770560 timeout 120: POST'ing to https://outlook.office365.com/EWS/Exchange.asmx after 0s sleep
2025-01-15 18:17:49 - DEBUG - urllib3.connectionpool - _new_conn:1049 - Starting new HTTPS connection (1): outlook.office365.com:443
2025-01-15 18:17:50 - DEBUG - urllib3.connectionpool - _make_request:544 - https://outlook.office365.com:443 "POST /EWS/Exchange.asmx HTTP/1.1" 401 0
2025-01-15 18:17:50 - DEBUG - exchangelib.util - post_ratelimited:862 - Timeout: 120
Session: 84582
Thread: 140214710770560
Auth type: None
URL: https://outlook.office365.com/EWS/Exchange.asmx
HTTP adapter: 
Streaming: False
Response time: 0.23992178899788996
Status code: 401
Request headers: {'User-Agent': 'exchangelib/5.5.0 (python-requests/2.32.3)', 'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Type': 'text/xml; charset=utf-8', 'X-AnchorMailbox': '[email protected]', 'Content-Length': '1214'}
Response headers: {'Content-Length': '0', 'Server': 'Microsoft-HTTPAPI/2.0', 'X-BEServer': 'AS8PR08MB6357', 'X-NanoProxy': '1,1', 'Request-Id': '3d6befc1-e20e-e7b9-1ac1-75aae8bdd77b', 'X-CalculatedFETarget': 'AS9PR07CU001.internal.outlook.com', 'X-BeSku': 'WCS6', 'X-BackEndHttpStatus': '401,401', 'BasicChallengeAdded': 'True', 'MS-CV': 'we9rPQ7iuecawXWq6L3Xew.1.1', 'X-CalculatedBETarget': 'AS8PR08MB6357.eurprd08.prod.outlook.com', 'X-DiagInfo': 'AS8PR08MB6357', 'X-FEEFZInfo': 'DHR', 'X-UserType': 'Business', 'X-FEProxyInfo': 'AS9PR07CA0001', 'X-FEServer': 'DUZPR01CA0137', 'X-Proxy-BackendServerStatus': '401', 'X-Proxy-RoutingCorrectness': '1', 'X-RUM-NotUpdateQueriedPath': '1', 'X-RUM-NotUpdateQueriedDbCopy': '1', 'X-RUM-Validated': '1', 'X-FirstHopCafeEFZ': 'DUB', 'Strict-Transport-Security': 'max-age=31536000; includeSubDomains', 'Set-Cookie': 'exchangecookie=a7a24814fef441248023763bc5787ec0; expires=Thu, 15-Jan-2026 18:17:50 GMT; path=/; secure; HttpOnly', 'WWW-Authenticate': 'Basic Realm=""', 'Date': 'Wed, 15 Jan 2025 18:17:49 GMT'}
2025-01-15 18:17:50 - DEBUG - exchangelib.util.xml - post_ratelimited:863 - Request XML: b'\[email protected]'
Response XML: b''
2025-01-15 18:17:50 - DEBUG - exchangelib.protocol - retire_session:279 - Server outlook.office365.com: Retiring session 84582
2025-01-15 18:17:50 - DEBUG - exchangelib.protocol - create_session:330 - Server outlook.office365.com: Created session 79172
2025-01-15 18:17:50 - DEBUG - exchangelib.protocol - release_session:261 - Server outlook.office365.com: Releasing session 79172
=========================== short test summary info ============================
FAILED tests/test_exchange_cba.py::test_exchange_connection - exchangelib.err...
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
============================== 1 failed in 0.78s ===============================
@ecederstrand
Copy link
Owner

I haven't tried CBA myself, and I have no easy way to test it myself, unfortunately. I have very little knowledge about how CBA workes, but is it possible that the certificate is valid but has not been granted access to EWS?

@darknetehf
Copy link
Author

I don't think it's an exchangelib problem per se, but it could be that I am using it incorrectly.
The error code 401 makes me think that there is indeed something wrong with the cert.

With tokens, the lib works just fine:
tokens

I uploaded the certificate in the same app registration area:
certificates

And the app has all permissions:
Image

I believe there is nothing more needed. I already use a Python MSAL lib and I was able to switch from tokens to certificates the same way, without any problem. But I will keep looking and post updates if I find anything interesting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants