Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-msal for openSUSE:Factory checked in at 2026-02-07 15:33:18 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-msal (Old) and /work/SRC/openSUSE:Factory/.python-msal.new.1670 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-msal" Sat Feb 7 15:33:18 2026 rev:33 rq:1331689 version:1.35.0~b1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-msal/python-msal.changes 2025-10-23 18:31:37.939542090 +0200 +++ /work/SRC/openSUSE:Factory/.python-msal.new.1670/python-msal.changes 2026-02-07 15:33:39.785962896 +0100 @@ -1,0 +2,26 @@ +Fri Feb 6 09:38:14 UTC 2026 - John Paul Adrian Glaubitz <[email protected]> + +- Update to version 1.35.0b1 + * The managed identity code path no longer has a dependency on the + socket.getfqdn(). No API change is needed. Existing MSAL-powered + apps will automatically pick up this new behavior. + * This version of MSAL Python will pick up PyMsalRuntime 0.20.*. + No API change is needed. Existing MSAL-powered apps will + automatically pick up this new behavior. + * The thumbprint name-value pair in the client_credential parameter + becomes optional now. See API docs for usage. + * ROPC deprecation by @Ugonnaak1 in (#855) + * Test case for token response scope differing from token request + scope by @rayluo in (#856) + * Update pymsalruntime version range to handle the latest 0.20.0 release + by @DharshanBJ in (#858) + * Document how to enable sha256 for client credential by @rayluo in (#833) + * Remove the reliance on getfqdn() by @rayluo in (#859) + * Thumbprint for certificate made optional by @vi7us in (#835) + * Support Python 3.14 by @rayluo in (#861) + * Explicitly remove issuer from the OIDC discovery response + by @rayluo in (#863) + * Suppress CodeQL warning by @bgavrilMS in (#867) +- Override upstream version with 1.35.0~b1 + +------------------------------------------------------------------- Old: ---- msal-1.34.0.tar.gz New: ---- msal-1.35.0b1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-msal.spec ++++++ --- /var/tmp/diff_new_pack.UxZk3P/_old 2026-02-07 15:33:40.657999125 +0100 +++ /var/tmp/diff_new_pack.UxZk3P/_new 2026-02-07 15:33:40.657999125 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-msal # -# Copyright (c) 2025 SUSE LLC +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -15,15 +15,18 @@ # Please submit bugfixes or comments via https://bugs.opensuse.org/ # + +%define realversion 1.35.0b1 + %{?sle15_python_module_pythons} Name: python-msal -Version: 1.34.0 +Version: 1.35.0~b1 Release: 0 Summary: Microsoft Authentication Library (MSAL) for Python License: MIT Group: Development/Languages/Python URL: https://github.com/AzureAD/microsoft-authentication-library-for-python -Source: https://files.pythonhosted.org/packages/source/m/msal/msal-%{version}.tar.gz +Source: https://files.pythonhosted.org/packages/source/m/msal/msal-%{realversion}.tar.gz BuildRequires: %{python_module devel} BuildRequires: %{python_module pip} BuildRequires: %{python_module setuptools} @@ -50,7 +53,7 @@ standard OAuth2 and OpenID Connect. %prep -%setup -q -n msal-%{version} +%setup -q -n msal-%{realversion} %build %pyproject_wheel ++++++ msal-1.34.0.tar.gz -> msal-1.35.0b1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/PKG-INFO new/msal-1.35.0b1/PKG-INFO --- old/msal-1.34.0/PKG-INFO 2025-09-23 01:05:43.740560000 +0200 +++ new/msal-1.35.0b1/PKG-INFO 2026-01-07 00:51:47.246401800 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: msal -Version: 1.34.0 +Version: 1.35.0b1 Summary: The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect. Home-page: https://github.com/AzureAD/microsoft-authentication-library-for-python Author: Microsoft Corporation @@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Requires-Python: >=3.8 @@ -29,9 +30,9 @@ Requires-Dist: PyJWT[crypto]<3,>=1.0.0 Requires-Dist: cryptography<49,>=2.5 Provides-Extra: broker -Requires-Dist: pymsalruntime<0.19,>=0.14; (python_version >= "3.6" and platform_system == "Windows") and extra == "broker" -Requires-Dist: pymsalruntime<0.19,>=0.17; (python_version >= "3.8" and platform_system == "Darwin") and extra == "broker" -Requires-Dist: pymsalruntime<0.19,>=0.18; (python_version >= "3.8" and platform_system == "Linux") and extra == "broker" +Requires-Dist: pymsalruntime<0.21,>=0.14; (python_version >= "3.8" and platform_system == "Windows") and extra == "broker" +Requires-Dist: pymsalruntime<0.21,>=0.17; (python_version >= "3.8" and platform_system == "Darwin") and extra == "broker" +Requires-Dist: pymsalruntime<0.21,>=0.18; (python_version >= "3.8" and platform_system == "Linux") and extra == "broker" Dynamic: license-file # Microsoft Authentication Library (MSAL) for Python diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/msal/application.py new/msal-1.35.0b1/msal/application.py --- old/msal-1.34.0/msal/application.py 2025-09-23 01:05:37.000000000 +0200 +++ new/msal-1.35.0b1/msal/application.py 2026-01-07 00:51:42.000000000 +0100 @@ -66,10 +66,24 @@ except: return raw +def _extract_cert_and_thumbprints(cert): + # Cert concepts https://security.stackexchange.com/a/226758/125264 + from cryptography.hazmat.primitives import hashes, serialization + cert_pem = cert.public_bytes( # Requires cryptography 1.0+ + encoding=serialization.Encoding.PEM).decode() + x5c = [ + '\n'.join( + cert_pem.splitlines() + [1:-1] # Strip the "--- header ---" and "--- footer ---" + ) + ] + # https://cryptography.io/en/latest/x509/reference/#x-509-certificate-object - Requires cryptography 0.7+ + sha256_thumbprint = cert.fingerprint(hashes.SHA256()).hex() + sha1_thumbprint = cert.fingerprint(hashes.SHA1()).hex() # CodeQL [SM02167] for legacy support such as ADFS + return sha256_thumbprint, sha1_thumbprint, x5c def _parse_pfx(pfx_path, passphrase_bytes): # Cert concepts https://security.stackexchange.com/a/226758/125264 - from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.serialization import pkcs12 with open(pfx_path, 'rb') as f: private_key, cert, _ = pkcs12.load_key_and_certificates( # cryptography 2.5+ @@ -77,13 +91,7 @@ f.read(), passphrase_bytes) if not (private_key and cert): raise ValueError("Your PFX file shall contain both private key and cert") - cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM).decode() # cryptography 1.0+ - x5c = [ - '\n'.join(cert_pem.splitlines()[1:-1]) # Strip the "--- header ---" and "--- footer ---" - ] - sha256_thumbprint = cert.fingerprint(hashes.SHA256()).hex() # cryptography 0.7+ - sha1_thumbprint = cert.fingerprint(hashes.SHA1()).hex() # cryptography 0.7+ - # https://cryptography.io/en/latest/x509/reference/#x-509-certificate-object + sha256_thumbprint, sha1_thumbprint, x5c = _extract_cert_and_thumbprints(cert) return private_key, sha256_thumbprint, sha1_thumbprint, x5c @@ -280,12 +288,20 @@ .. admonition:: Support using a certificate in X.509 (.pem) format + Deprecated because it uses SHA-1 thumbprint, + unless you are still using ADFS which supports SHA-1 thumbprint only. + Please use the .pfx option documented later in this page. + Feed in a dict in this form:: { "private_key": "...-----BEGIN PRIVATE KEY-----... in PEM format", - "thumbprint": "A1B2C3D4E5F6...", - "passphrase": "Passphrase if the private_key is encrypted (Optional. Added in version 1.6.0)", + "thumbprint": "An SHA-1 thumbprint such as A1B2C3D4E5F6..." + "Changed in version 1.35.0, if thumbprint is absent" + "and a public_certificate is present, MSAL will" + "automatically calculate an SHA-256 thumbprint instead.", + "passphrase": "Needed if the private_key is encrypted (Added in version 1.6.0)", + "public_certificate": "...-----BEGIN CERTIFICATE-----...", # Needed if you use Subject Name/Issuer auth. Added in version 0.5.0. } MSAL Python requires a "private_key" in PEM format. @@ -296,25 +312,11 @@ The thumbprint is available in your app's registration in Azure Portal. Alternatively, you can `calculate the thumbprint <https://github.com/Azure/azure-sdk-for-python/blob/07d10639d7e47f4852eaeb74aef5d569db499d6e/sdk/identity/azure-identity/azure/identity/_credentials/certificate.py#L94-L97>`_. - .. admonition:: Support Subject Name/Issuer Auth with a cert in .pem - - `Subject Name/Issuer Auth - <https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/60>`_ - is an approach to allow easier certificate rotation. - - *Added in version 0.5.0*:: - - { - "private_key": "...-----BEGIN PRIVATE KEY-----... in PEM format", - "thumbprint": "A1B2C3D4E5F6...", - "public_certificate": "...-----BEGIN CERTIFICATE-----...", - "passphrase": "Passphrase if the private_key is encrypted (Optional. Added in version 1.6.0)", - } - ``public_certificate`` (optional) is public key certificate - which will be sent through 'x5c' JWT header only for - subject name and issuer authentication to support cert auto rolls. - + which will be sent through 'x5c' JWT header. + This is useful when you use `Subject Name/Issuer Authentication + <https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/60>`_ + which is an approach to allow easier certificate rotation. Per `specs <https://tools.ietf.org/html/rfc7515#section-4.1.6>`_, "the certificate containing the public key corresponding to the key used to digitally sign the @@ -338,11 +340,14 @@ .. admonition:: Supporting reading client certificates from PFX files + This usage will automatically use SHA-256 thumbprint of the certificate. + *Added in version 1.29.0*: Feed in a dictionary containing the path to a PFX file:: { - "private_key_pfx_path": "/path/to/your.pfx", + "private_key_pfx_path": "/path/to/your.pfx", # Added in version 1.29.0 + "public_certificate": True, # Only needed if you use Subject Name/Issuer auth. Added in version 1.30.0 "passphrase": "Passphrase if the private_key is encrypted (Optional)", } @@ -350,17 +355,11 @@ openssl pkcs12 -export -out certificate.pfx -inkey privateKey.key -in certificate.pem - .. admonition:: Support Subject Name/Issuer Auth with a cert in .pfx - - *Added in version 1.30.0*: + `Subject Name/Issuer Auth + <https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/60>`_ + is an approach to allow easier certificate rotation. If your .pfx file contains both the private key and public cert, - you can opt in for Subject Name/Issuer Auth like this:: - - { - "private_key_pfx_path": "/path/to/your.pfx", - "public_certificate": True, - "passphrase": "Passphrase if the private_key is encrypted (Optional)", - } + you can opt in for Subject Name/Issuer Auth by setting "public_certificate" to ``True``. :type client_credential: Union[dict, str, None] @@ -815,15 +814,30 @@ passphrase_bytes) if client_credential.get("public_certificate") is True and x5c: headers["x5c"] = x5c - elif ( - client_credential.get("private_key") # PEM blob - and client_credential.get("thumbprint")): - sha1_thumbprint = client_credential["thumbprint"] - if passphrase_bytes: - private_key = _load_private_key_from_pem_str( + elif client_credential.get("private_key"): # PEM blob + private_key = ( # handles both encrypted and unencrypted + _load_private_key_from_pem_str( client_credential['private_key'], passphrase_bytes) - else: # PEM without passphrase - private_key = client_credential['private_key'] + if passphrase_bytes + else client_credential['private_key'] + ) + + # Determine thumbprints based on what's provided + if client_credential.get("thumbprint"): + # User provided a thumbprint - use it as SHA-1 (legacy/manual approach) + sha1_thumbprint = client_credential["thumbprint"] + sha256_thumbprint = None + elif isinstance(client_credential.get('public_certificate'), str): + # No thumbprint provided, but we have a certificate to calculate thumbprints + from cryptography import x509 + cert = x509.load_pem_x509_certificate( + _str2bytes(client_credential['public_certificate'])) + sha256_thumbprint, sha1_thumbprint, headers["x5c"] = ( + _extract_cert_and_thumbprints(cert)) + else: + raise ValueError( + "You must provide either 'thumbprint' or 'public_certificate' " + "from which the thumbprint can be calculated.") else: raise ValueError( "client_credential needs to follow this format " @@ -1840,7 +1854,17 @@ - A successful response would contain "access_token" key, - an error response would contain "error" and usually "error_description". + + [Deprecated] This API is deprecated for public client flows and will be + removed in a future release. Use a more secure flow instead. + Migration guide: https://aka.ms/msal-ropc-migration + """ + is_confidential_app = self.client_credential or isinstance( + self, ConfidentialClientApplication) + if not is_confidential_app: + warnings.warn("""This API has been deprecated for public client flows, please use a more secure flow. + See https://aka.ms/msal-ropc-migration for migration guidance""", DeprecationWarning) claims = _merge_claims_challenge_and_capabilities( self._client_capabilities, claims_challenge) if self._enable_broker and sys.platform in ("win32", "darwin"): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/msal/authority.py new/msal-1.35.0b1/msal/authority.py --- old/msal-1.34.0/msal/authority.py 2025-09-23 01:05:37.000000000 +0200 +++ new/msal-1.35.0b1/msal/authority.py 2026-01-07 00:51:42.000000000 +0100 @@ -93,6 +93,7 @@ .format(authority_url) ) + " Also please double check your tenant name or GUID is correct." raise ValueError(error_message) + openid_config.pop("issuer", None) # Not used in MSAL.py, so remove it therefore no need to validate it logger.debug( 'openid_config("%s") = %s', tenant_discovery_endpoint, openid_config) self.authorization_endpoint = openid_config['authorization_endpoint'] @@ -178,7 +179,7 @@ def canonicalize(authority_or_auth_endpoint): # Returns (url_parsed_result, hostname_in_lowercase, tenant) authority = urlparse(authority_or_auth_endpoint) - if authority.scheme == "https": + if authority.scheme == "https" and authority.hostname: parts = authority.path.split("/") first_part = parts[1] if len(parts) >= 2 and parts[1] else None if authority.hostname.endswith(_CIAM_DOMAIN_SUFFIX): # CIAM @@ -192,7 +193,7 @@ return authority, authority.hostname, parts[1] raise ValueError( "Your given address (%s) should consist of " - "an https url with a minimum of one segment in a path: e.g. " + "an https url with hostname and a minimum of one segment in a path: e.g. " "https://login.microsoftonline.com/{tenant} " "or https://{tenant_name}.ciamlogin.com/{tenant} " "or https://{tenant_name}.b2clogin.com/{tenant_name}.onmicrosoft.com/policy" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/msal/broker.py new/msal-1.35.0b1/msal/broker.py --- old/msal-1.34.0/msal/broker.py 2025-09-23 01:05:37.000000000 +0200 +++ new/msal-1.35.0b1/msal/broker.py 2026-01-07 00:51:42.000000000 +0100 @@ -145,12 +145,20 @@ params.set_additional_parameter("msal_client_ver", __version__) return params +def _set_redirect_uri_for_linux(params): + if sys.platform == "linux": + # This is required by Linux Java Broker to set a non-empty valid redirect_uri + params.set_redirect_uri( + "https://login.microsoftonline.com/common/oauth2/nativeclient" + ) + def _signin_silently( authority, client_id, scopes, correlation_id=None, claims=None, enable_msa_pt=False, auth_scheme=None, **kwargs): params = _build_msal_runtime_auth_params(client_id, authority) + _set_redirect_uri_for_linux(params) params.set_requested_scopes(scopes) if claims: params.set_decoded_claims(claims) @@ -240,6 +248,7 @@ if account is None: return params = _build_msal_runtime_auth_params(client_id, authority) + _set_redirect_uri_for_linux(params) params.set_requested_scopes(scopes) if claims: params.set_decoded_claims(claims) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/msal/managed_identity.py new/msal-1.35.0b1/msal/managed_identity.py --- old/msal-1.34.0/msal/managed_identity.py 2025-09-23 01:05:37.000000000 +0200 +++ new/msal-1.35.0b1/msal/managed_identity.py 2026-01-07 00:51:42.000000000 +0100 @@ -6,7 +6,6 @@ import json import logging import os -import socket import sys import time from urllib.parse import urlparse # Python 3+ @@ -146,7 +145,10 @@ (like what a ``PublicClientApplication`` does), not a token with application permissions for an app. """ - __instance, _tenant = None, "managed_identity" # Placeholders + __instance = "localhost" # We used to get this value from socket.getfqdn() + # but it is unreliable because getfqdn() either hangs or returns empty value + # on some misconfigured machines + _tenant = "managed_identity" _TOKEN_SOURCE = "token_source" _TOKEN_SOURCE_IDP = "identity_provider" _TOKEN_SOURCE_CACHE = "cache" @@ -252,11 +254,6 @@ self._token_cache = token_cache or TokenCache() self._client_capabilities = client_capabilities - def _get_instance(self): - if self.__instance is None: - self.__instance = socket.getfqdn() # Moved from class definition to here - return self.__instance - def acquire_token_for_client( self, *, @@ -302,7 +299,7 @@ target=[resource], query=dict( client_id=client_id_in_cache, - environment=self._get_instance(), + environment=self.__instance, realm=self._tenant, home_account_id=None, ), @@ -344,7 +341,7 @@ client_id=client_id_in_cache, scope=[resource], token_endpoint="https://{}/{}".format( - self._get_instance(), self._tenant), + self.__instance, self._tenant), response=result, params={}, data={}, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/msal/sku.py new/msal-1.35.0b1/msal/sku.py --- old/msal-1.34.0/msal/sku.py 2025-09-23 01:05:37.000000000 +0200 +++ new/msal-1.35.0b1/msal/sku.py 2026-01-07 00:51:42.000000000 +0100 @@ -2,5 +2,5 @@ """ # The __init__.py will import this. Not the other way around. -__version__ = "1.34.0" +__version__ = "1.35.0b1" SKU = "MSAL.Python" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/msal.egg-info/PKG-INFO new/msal-1.35.0b1/msal.egg-info/PKG-INFO --- old/msal-1.34.0/msal.egg-info/PKG-INFO 2025-09-23 01:05:43.000000000 +0200 +++ new/msal-1.35.0b1/msal.egg-info/PKG-INFO 2026-01-07 00:51:47.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.4 Name: msal -Version: 1.34.0 +Version: 1.35.0b1 Summary: The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect. Home-page: https://github.com/AzureAD/microsoft-authentication-library-for-python Author: Microsoft Corporation @@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Requires-Python: >=3.8 @@ -29,9 +30,9 @@ Requires-Dist: PyJWT[crypto]<3,>=1.0.0 Requires-Dist: cryptography<49,>=2.5 Provides-Extra: broker -Requires-Dist: pymsalruntime<0.19,>=0.14; (python_version >= "3.6" and platform_system == "Windows") and extra == "broker" -Requires-Dist: pymsalruntime<0.19,>=0.17; (python_version >= "3.8" and platform_system == "Darwin") and extra == "broker" -Requires-Dist: pymsalruntime<0.19,>=0.18; (python_version >= "3.8" and platform_system == "Linux") and extra == "broker" +Requires-Dist: pymsalruntime<0.21,>=0.14; (python_version >= "3.8" and platform_system == "Windows") and extra == "broker" +Requires-Dist: pymsalruntime<0.21,>=0.17; (python_version >= "3.8" and platform_system == "Darwin") and extra == "broker" +Requires-Dist: pymsalruntime<0.21,>=0.18; (python_version >= "3.8" and platform_system == "Linux") and extra == "broker" Dynamic: license-file # Microsoft Authentication Library (MSAL) for Python diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/msal.egg-info/SOURCES.txt new/msal-1.35.0b1/msal.egg-info/SOURCES.txt --- old/msal-1.34.0/msal.egg-info/SOURCES.txt 2025-09-23 01:05:43.000000000 +0200 +++ new/msal-1.35.0b1/msal.egg-info/SOURCES.txt 2026-01-07 00:51:47.000000000 +0100 @@ -47,6 +47,7 @@ tests/test_mex.py tests/test_mi.py tests/test_oidc.py +tests/test_optional_thumbprint.py tests/test_throttled_http_client.py tests/test_token_cache.py tests/test_wstrust.py \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/msal.egg-info/requires.txt new/msal-1.35.0b1/msal.egg-info/requires.txt --- old/msal-1.34.0/msal.egg-info/requires.txt 2025-09-23 01:05:43.000000000 +0200 +++ new/msal-1.35.0b1/msal.egg-info/requires.txt 2026-01-07 00:51:47.000000000 +0100 @@ -4,11 +4,11 @@ [broker] -[broker:python_version >= "3.6" and platform_system == "Windows"] -pymsalruntime<0.19,>=0.14 - [broker:python_version >= "3.8" and platform_system == "Darwin"] -pymsalruntime<0.19,>=0.17 +pymsalruntime<0.21,>=0.17 [broker:python_version >= "3.8" and platform_system == "Linux"] -pymsalruntime<0.19,>=0.18 +pymsalruntime<0.21,>=0.18 + +[broker:python_version >= "3.8" and platform_system == "Windows"] +pymsalruntime<0.21,>=0.14 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/setup.cfg new/msal-1.35.0b1/setup.cfg --- old/msal-1.34.0/setup.cfg 2025-09-23 01:05:43.740560000 +0200 +++ new/msal-1.35.0b1/setup.cfg 2026-01-07 00:51:47.247401700 +0100 @@ -22,6 +22,7 @@ Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 + Programming Language :: Python :: 3.14 License :: OSI Approved :: MIT License Operating System :: OS Independent project_urls = @@ -43,9 +44,9 @@ [options.extras_require] broker = - pymsalruntime>=0.14,<0.19; python_version>='3.6' and platform_system=='Windows' - pymsalruntime>=0.17,<0.19; python_version>='3.8' and platform_system=='Darwin' - pymsalruntime>=0.18,<0.19; python_version>='3.8' and platform_system=='Linux' + pymsalruntime>=0.14,<0.21; python_version>='3.8' and platform_system=='Windows' + pymsalruntime>=0.17,<0.21; python_version>='3.8' and platform_system=='Darwin' + pymsalruntime>=0.18,<0.21; python_version>='3.8' and platform_system=='Linux' [options.packages.find] exclude = diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/tests/test_application.py new/msal-1.35.0b1/tests/test_application.py --- old/msal-1.34.0/tests/test_application.py 2025-09-23 01:05:37.000000000 +0200 +++ new/msal-1.35.0b1/tests/test_application.py 2026-01-07 00:51:42.000000000 +0100 @@ -875,3 +875,50 @@ parent_window_handle=app.CONSOLE_WINDOW_HANDLE, ) self.assertEqual(result.get("error"), "broker_error") + + +class MismatchingScopeTestCase(unittest.TestCase): + """Test cache behavior when HTTP response scope differs from requested scope""" + + def test_token_should_be_cached_with_response_scope(self): + """Based on https://datatracker.ietf.org/doc/html/rfc6749#section-3.3 + authorization server may issue an access token with different scope. + For example, eSTS normalizes scopes by adding or removing trailing slash. + Calling app is supposed to use the normalized scope for subsequent calls. + """ + + # Create a fresh app instance + app = ConfidentialClientApplication( + "client_id", client_credential="secret", + authority="https://login.microsoftonline.com/common") + + # Mocked request: ask for "invalid_scope" scope but receive "valid_scope1 valid_scope2" scope in response + def mock_post(url, headers=None, *args, **kwargs): + return MinimalResponse(status_code=200, text=json.dumps({ + "access_token": "AT_with_valid_scope1_valid_scope2_scopes", + "expires_in": 3600, + "scope": "valid_scope1 valid_scope2", # Response scope differs from requested scope + "token_type": "Bearer" + })) + + result1 = app.acquire_token_for_client(["invalid_scope"], post=mock_post) + self.assertEqual(result1[app._TOKEN_SOURCE], app._TOKEN_SOURCE_IDP) + self.assertEqual("AT_with_valid_scope1_valid_scope2_scopes", result1.get("access_token")) + self.assertEqual(["valid_scope1", "valid_scope2"], result1.get("scope").split()) # Scope from response + + # Second request: ask for same "invalid_scope" scope again + # Since cached token has "valid_scope1 valid_scope2" scopes, it shouldn't match the "invalid_scope" request + # This should go to IDP again and receive the same response + result2 = app.acquire_token_for_client(["invalid_scope"], post=mock_post) + # Should get a new token from IDP, not from cache + self.assertEqual(result2[app._TOKEN_SOURCE], app._TOKEN_SOURCE_IDP) + self.assertEqual("AT_with_valid_scope1_valid_scope2_scopes", result2.get("access_token")) + self.assertEqual(["valid_scope1", "valid_scope2"], result2.get("scope").split()) + + # Third and fourth requests: ask for individual valid scopes + # Should hit cache for the token that has "valid_scope1 valid_scope2" scopes + for scope in ["valid_scope1", "valid_scope2"]: + result = app.acquire_token_for_client([scope]) + self.assertEqual(result[app._TOKEN_SOURCE], app._TOKEN_SOURCE_CACHE) + self.assertEqual("AT_with_valid_scope1_valid_scope2_scopes", result.get("access_token")) + self.assertIsNone(result.get("scope"), "scope field is not returned when token comes from cache") \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/tests/test_authority.py new/msal-1.35.0b1/tests/test_authority.py --- old/msal-1.34.0/tests/test_authority.py 2025-09-23 01:05:37.000000000 +0200 +++ new/msal-1.35.0b1/tests/test_authority.py 2026-01-07 00:51:42.000000000 +0100 @@ -190,6 +190,10 @@ with self.assertRaises(ValueError): canonicalize("https://no.tenant.example.com/") + def test_canonicalize_rejects_empty_host(self): + with self.assertRaises(ValueError): + canonicalize("https:///tenant") + @unittest.skipIf(os.getenv("TRAVIS_TAG"), "Skip network io during tagged release") class TestAuthorityInternalHelperUserRealmDiscovery(unittest.TestCase): diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/tests/test_e2e.py new/msal-1.35.0b1/tests/test_e2e.py --- old/msal-1.34.0/tests/test_e2e.py 2025-09-23 01:05:37.000000000 +0200 +++ new/msal-1.35.0b1/tests/test_e2e.py 2026-01-07 00:51:42.000000000 +0100 @@ -841,14 +841,14 @@ class WorldWideTestCase(LabBasedTestCase): - _ADFS_LABS_DECOMMISSIONED = "ADFS labs were decommissioned since July 2025 until further notice" + _ADFS_LABS_UNAVAILABLE = "ADFS labs were temporarily down since July 2025 until further notice" def test_aad_managed_user(self): # Pure cloud config = self.get_lab_user(usertype="cloud") config["password"] = self.get_lab_user_secret(config["lab_name"]) self._test_username_password(**config) - @unittest.skip(_ADFS_LABS_DECOMMISSIONED) + @unittest.skip(_ADFS_LABS_UNAVAILABLE) def test_adfs4_fed_user(self): config = self.get_lab_user(usertype="federated", federationProvider="ADFSv4") config["password"] = self.get_lab_user_secret(config["lab_name"]) @@ -866,7 +866,7 @@ config["password"] = self.get_lab_user_secret(config["lab_name"]) self._test_username_password(**config) - @unittest.skip(_ADFS_LABS_DECOMMISSIONED) + @unittest.skip(_ADFS_LABS_UNAVAILABLE) def test_adfs2019_fed_user(self): try: config = self.get_lab_user(usertype="federated", federationProvider="ADFSv2019") @@ -895,7 +895,7 @@ prompt="select_account", # In MSAL Python, this resets login_hint )) - @unittest.skip(_ADFS_LABS_DECOMMISSIONED) + @unittest.skip(_ADFS_LABS_UNAVAILABLE) def test_ropc_adfs2019_onprem(self): # Configuration is derived from https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/4.7.0/tests/Microsoft.Identity.Test.Common/TestConstants.cs#L250-L259 config = self.get_lab_user(usertype="onprem", federationProvider="ADFSv2019") @@ -904,7 +904,7 @@ config["password"] = self.get_lab_user_secret(config["lab_name"]) self._test_username_password(**config) - @unittest.skip(_ADFS_LABS_DECOMMISSIONED) + @unittest.skip(_ADFS_LABS_UNAVAILABLE) def test_adfs2019_onprem_acquire_token_by_auth_code(self): """When prompted, you can manually login using this account: @@ -918,7 +918,7 @@ config["port"] = 8080 self._test_acquire_token_by_auth_code(**config) - @unittest.skip(_ADFS_LABS_DECOMMISSIONED) + @unittest.skip(_ADFS_LABS_UNAVAILABLE) def test_adfs2019_onprem_acquire_token_by_auth_code_flow(self): config = self.get_lab_user(usertype="onprem", federationProvider="ADFSv2019") self._test_acquire_token_by_auth_code_flow(**dict( @@ -928,7 +928,7 @@ port=8080, )) - @unittest.skip(_ADFS_LABS_DECOMMISSIONED) + @unittest.skip(_ADFS_LABS_UNAVAILABLE) def test_adfs2019_onprem_acquire_token_interactive(self): config = self.get_lab_user(usertype="onprem", federationProvider="ADFSv2019") self._test_acquire_token_interactive(**dict( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/tests/test_mi.py new/msal-1.35.0b1/tests/test_mi.py --- old/msal-1.34.0/tests/test_mi.py 2025-09-23 01:05:37.000000000 +0200 +++ new/msal-1.35.0b1/tests/test_mi.py 2026-01-07 00:51:42.000000000 +0100 @@ -190,8 +190,12 @@ headers={'Metadata': 'true'}, ) - @patch("msal.managed_identity.socket.getfqdn", new=lambda: "MixedCaseHostName") - def test_happy_path_of_windows_vm(self): + @patch.object(ManagedIdentityClient, "_ManagedIdentityClient__instance", "MixedCaseHostName") + def test_happy_path_of_theoretical_mixed_case_hostname(self): + """Historically, we used to get the host name from socket.getfqdn(), + which could return a mixed-case host name on Windows. + Although we no longer use getfqdn(), we still keep this test case to ensure we tolerate it. + """ self.test_happy_path_of_vm() @patch.dict(os.environ, {"AZURE_POD_IDENTITY_AUTHORITY_HOST": "http://localhost:1234//"}) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/msal-1.34.0/tests/test_optional_thumbprint.py new/msal-1.35.0b1/tests/test_optional_thumbprint.py --- old/msal-1.34.0/tests/test_optional_thumbprint.py 1970-01-01 01:00:00.000000000 +0100 +++ new/msal-1.35.0b1/tests/test_optional_thumbprint.py 2026-01-07 00:51:42.000000000 +0100 @@ -0,0 +1,215 @@ +import unittest +from unittest.mock import Mock, patch +from msal.application import ConfidentialClientApplication + + +@patch('msal.application.Authority') +@patch('msal.application.JwtAssertionCreator', new_callable=lambda: Mock( + return_value=Mock(create_regenerative_assertion=Mock(return_value="mock_jwt_assertion")))) +class TestClientCredentialWithOptionalThumbprint(unittest.TestCase): + """Test that thumbprint is optional when public_certificate is provided""" + + # Sample test certificate and private key (PEM format) + # These are minimal valid PEM structures for testing + test_private_key = """-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKj +MzEfYyjiWA4R4/M2bS1+fWIcPm15j7uo6xKvRr4PNx5bKMDFqMdW6/xfqFWX0nZK +-----END PRIVATE KEY-----""" + + test_certificate = """-----BEGIN CERTIFICATE----- +MIIC5jCCAc6gAwIBAgIJALdYQVsVsNZHMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV +BAMMC0V4YW1wbGUgQ0EwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAW +-----END CERTIFICATE-----""" + + def _setup_mocks(self, mock_authority_class, authority="https://login.microsoftonline.com/common"): + """Helper to setup Authority mock""" + # Setup Authority mock + mock_authority = Mock() + mock_authority.is_adfs = "adfs" in authority.lower() + + # Extract instance from authority URL + if mock_authority.is_adfs: + # For ADFS: https://adfs.contoso.com/adfs -> adfs.contoso.com + mock_authority.instance = authority.split("//")[1].split("/")[0] + mock_authority.token_endpoint = f"https://{mock_authority.instance}/adfs/oauth2/token" + mock_authority.authorization_endpoint = f"https://{mock_authority.instance}/adfs/oauth2/authorize" + else: + # For AAD: https://login.microsoftonline.com/common -> login.microsoftonline.com + mock_authority.instance = authority.split("//")[1].split("/")[0] + mock_authority.token_endpoint = f"https://{mock_authority.instance}/common/oauth2/v2.0/token" + mock_authority.authorization_endpoint = f"https://{mock_authority.instance}/common/oauth2/v2.0/authorize" + + mock_authority.device_authorization_endpoint = None + mock_authority_class.return_value = mock_authority + + return mock_authority + + def _setup_certificate_mocks(self, mock_extract, mock_load_cert): + """Helper to setup certificate parsing mocks""" + # Mock certificate loading + mock_cert = Mock() + mock_load_cert.return_value = mock_cert + + # Mock _extract_cert_and_thumbprints to return thumbprints + mock_extract.return_value = ( + "mock_sha256_thumbprint", # sha256_thumbprint + "mock_sha1_thumbprint", # sha1_thumbprint + ["mock_x5c_value"] # x5c + ) + + def _verify_assertion_params(self, mock_jwt_creator_class, expected_algorithm, + expected_thumbprint_type, expected_thumbprint_value=None, + has_x5c=False): + """Helper to verify JwtAssertionCreator was called with correct params""" + mock_jwt_creator_class.assert_called_once() + call_args = mock_jwt_creator_class.call_args + + # Verify algorithm + self.assertEqual(call_args[1]['algorithm'], expected_algorithm) + + # Verify thumbprint type + if expected_thumbprint_type == 'sha256': + self.assertIn('sha256_thumbprint', call_args[1]) + self.assertNotIn('sha1_thumbprint', call_args[1]) + elif expected_thumbprint_type == 'sha1': + self.assertIn('sha1_thumbprint', call_args[1]) + self.assertNotIn('sha256_thumbprint', call_args[1]) + if expected_thumbprint_value: + self.assertEqual(call_args[1]['sha1_thumbprint'], expected_thumbprint_value) + + # Verify x5c header if expected + if has_x5c: + self.assertIn('headers', call_args[1]) + self.assertIn('x5c', call_args[1]['headers']) + + return call_args + + @patch('cryptography.x509.load_pem_x509_certificate') + @patch('msal.application._extract_cert_and_thumbprints') + def test_pem_with_certificate_only_uses_sha256( + self, mock_extract, mock_load_cert, mock_jwt_creator_class, mock_authority_class): + """Test that providing only public_certificate (no thumbprint) uses SHA-256""" + authority = "https://login.microsoftonline.com/common" + self._setup_mocks(mock_authority_class, authority) + self._setup_certificate_mocks(mock_extract, mock_load_cert) + + # Create app with certificate credential WITHOUT thumbprint + app = ConfidentialClientApplication( + client_id="my_client_id", + client_credential={ + "private_key": self.test_private_key, + "public_certificate": self.test_certificate, + # Note: NO thumbprint provided + }, + authority=authority + ) + + # Verify SHA-256 with PS256 algorithm is used + self._verify_assertion_params( + mock_jwt_creator_class, + expected_algorithm='PS256', + expected_thumbprint_type='sha256', + has_x5c=True + ) + + def test_pem_with_manual_thumbprint_uses_sha1( + self, mock_jwt_creator_class, mock_authority_class): + """Test that providing manual thumbprint (no certificate) uses SHA-1""" + authority = "https://login.microsoftonline.com/common" + self._setup_mocks(mock_authority_class, authority) + + # Create app with manual thumbprint (legacy approach) + manual_thumbprint = "A1B2C3D4E5F6" + app = ConfidentialClientApplication( + client_id="my_client_id", + client_credential={ + "private_key": self.test_private_key, + "thumbprint": manual_thumbprint, + # Note: NO public_certificate provided + }, + authority=authority + ) + + # Verify SHA-1 with RS256 algorithm is used + self._verify_assertion_params( + mock_jwt_creator_class, + expected_algorithm='RS256', + expected_thumbprint_type='sha1', + expected_thumbprint_value=manual_thumbprint + ) + + def test_pem_with_both_uses_manual_thumbprint_as_sha1( + self, mock_jwt_creator_class, mock_authority_class): + """Test that providing both thumbprint and certificate prefers manual thumbprint (SHA-1)""" + authority = "https://login.microsoftonline.com/common" + self._setup_mocks(mock_authority_class, authority) + + # Create app with BOTH thumbprint and certificate + manual_thumbprint = "A1B2C3D4E5F6" + app = ConfidentialClientApplication( + client_id="my_client_id", + client_credential={ + "private_key": self.test_private_key, + "thumbprint": manual_thumbprint, + "public_certificate": self.test_certificate, + }, + authority=authority + ) + + # Verify manual thumbprint takes precedence (backward compatibility) + self._verify_assertion_params( + mock_jwt_creator_class, + expected_algorithm='RS256', + expected_thumbprint_type='sha1', + expected_thumbprint_value=manual_thumbprint, + has_x5c=True # x5c should still be present + ) + + @patch('cryptography.x509.load_pem_x509_certificate') + @patch('msal.application._extract_cert_and_thumbprints') + def test_pem_with_adfs_uses_sha1( + self, mock_extract, mock_load_cert, mock_jwt_creator_class, mock_authority_class): + """Test that ADFS authority uses SHA-1 even with SHA-256 thumbprint""" + authority = "https://adfs.contoso.com/adfs" + self._setup_mocks(mock_authority_class, authority) + self._setup_certificate_mocks(mock_extract, mock_load_cert) + + # Create app with certificate on ADFS + app = ConfidentialClientApplication( + client_id="my_client_id", + client_credential={ + "private_key": self.test_private_key, + "public_certificate": self.test_certificate, + }, + authority=authority + ) + + # ADFS should force SHA-1 with RS256 even though SHA-256 would be calculated + self._verify_assertion_params( + mock_jwt_creator_class, + expected_algorithm='RS256', + expected_thumbprint_type='sha1' + ) + + def test_pem_with_neither_raises_error(self, mock_jwt_creator_class, mock_authority_class): + """Test that providing neither thumbprint nor certificate raises ValueError""" + authority = "https://login.microsoftonline.com/common" + self._setup_mocks(mock_authority_class, authority) + + # Should raise ValueError when neither thumbprint nor certificate provided + with self.assertRaises(ValueError) as context: + app = ConfidentialClientApplication( + client_id="my_client_id", + client_credential={ + "private_key": self.test_private_key, + # Note: NO thumbprint and NO public_certificate + }, + authority=authority + ) + + self.assertIn("thumbprint", str(context.exception).lower()) + self.assertIn("public_certificate", str(context.exception).lower()) + + +if __name__ == "__main__": + unittest.main()
