From 55483b20753ff519290ee5b1b3c3b1af5f03f01a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:59:51 +0000 Subject: [PATCH 1/8] Initial plan From 77fdd50bcfbe036a9c3d1070675fb95bc0eb9636 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:04:26 +0000 Subject: [PATCH 2/8] Add support for SHA256 certificate thumbprint with authority-based selection Co-authored-by: bgavrilMS <12273384+bgavrilMS@users.noreply.github.com> --- msal/application.py | 45 +++++++-- tests/test_optional_thumbprint.py | 163 ++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+), 9 deletions(-) diff --git a/msal/application.py b/msal/application.py index ba16df83..01582727 100644 --- a/msal/application.py +++ b/msal/application.py @@ -300,6 +300,10 @@ def __init__( "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.", + "thumbprint_sha256": "An SHA-256 thumbprint (Added in version 1.35.0). " + "If both thumbprint and thumbprint_sha256 are provided, " + "SHA-256 is used for AAD authorities (including B2C, CIAM), " + "and SHA-1 is used for ADFS and generic authorities.", "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. } @@ -823,10 +827,10 @@ def _build_client(self, client_credential, authority, skip_regional_client=False ) # 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 + if client_credential.get("thumbprint") or client_credential.get("thumbprint_sha256"): + # User provided one or both thumbprints - use them as-is + sha1_thumbprint = client_credential.get("thumbprint") + sha256_thumbprint = client_credential.get("thumbprint_sha256") elif isinstance(client_credential.get('public_certificate'), str): # No thumbprint provided, but we have a certificate to calculate thumbprints from cryptography import x509 @@ -836,7 +840,7 @@ def _build_client(self, client_credential, authority, skip_regional_client=False _extract_cert_and_thumbprints(cert)) else: raise ValueError( - "You must provide either 'thumbprint' or 'public_certificate' " + "You must provide either 'thumbprint', 'thumbprint_sha256', or 'public_certificate' " "from which the thumbprint can be calculated.") else: raise ValueError( @@ -846,13 +850,36 @@ def _build_client(self, client_credential, authority, skip_regional_client=False and isinstance(client_credential.get('public_certificate'), str) ): # Then we treat the public_certificate value as PEM content headers["x5c"] = extract_certs(client_credential['public_certificate']) - if sha256_thumbprint and not authority.is_adfs: + # Determine which thumbprint to use based on what's available and authority type + # Spec: If both thumbprints are provided: + # - Use SHA256 for AAD authorities (including B2C, CIAM) + # - Use SHA1 for ADFS and generic authorities + use_sha256 = False + if sha256_thumbprint and sha1_thumbprint: + # Both thumbprints provided - choose based on authority type + # Use SHA256 for AAD (including B2C, CIAM), SHA1 for ADFS and generic + from .authority import WELL_KNOWN_AUTHORITY_HOSTS, WELL_KNOWN_B2C_HOSTS, _CIAM_DOMAIN_SUFFIX + is_known_aad = authority.instance in WELL_KNOWN_AUTHORITY_HOSTS + is_b2c_or_ciam = ( + authority.instance.endswith(_CIAM_DOMAIN_SUFFIX) or + any(authority.instance.endswith("." + d) for d in WELL_KNOWN_B2C_HOSTS) + ) + # Use SHA256 for known AAD, B2C, or CIAM; SHA1 for ADFS and generic + use_sha256 = (is_known_aad or is_b2c_or_ciam) and not authority.is_adfs + elif sha256_thumbprint: + # Only SHA256 provided + use_sha256 = True + elif sha1_thumbprint: + # Only SHA1 provided + use_sha256 = False + else: + raise ValueError("You must provide either 'thumbprint' (SHA-1) or 'thumbprint_sha256' (SHA-256).") + + if use_sha256: assertion_params = { "algorithm": "PS256", "sha256_thumbprint": sha256_thumbprint, } - else: # Fall back - if not sha1_thumbprint: - raise ValueError("You shall provide a thumbprint in SHA1.") + else: assertion_params = { "algorithm": "RS256", "sha1_thumbprint": sha1_thumbprint, } diff --git a/tests/test_optional_thumbprint.py b/tests/test_optional_thumbprint.py index 56ec4013..996e008e 100644 --- a/tests/test_optional_thumbprint.py +++ b/tests/test_optional_thumbprint.py @@ -210,6 +210,169 @@ def test_pem_with_neither_raises_error(self, mock_jwt_creator_class, mock_author self.assertIn("thumbprint", str(context.exception).lower()) self.assertIn("public_certificate", str(context.exception).lower()) + def test_pem_with_thumbprint_sha256_only_uses_sha256( + self, mock_jwt_creator_class, mock_authority_class): + """Test that providing only thumbprint_sha256 uses SHA-256""" + authority = "https://login.microsoftonline.com/common" + self._setup_mocks(mock_authority_class, authority) + + # Create app with only SHA256 thumbprint + sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2" + app = ConfidentialClientApplication( + client_id="my_client_id", + client_credential={ + "private_key": self.test_private_key, + "thumbprint_sha256": sha256_thumbprint, + }, + 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' + ) + + def test_pem_with_both_thumbprints_aad_uses_sha256( + self, mock_jwt_creator_class, mock_authority_class): + """Test that with both thumbprints, AAD authority uses SHA-256""" + authority = "https://login.microsoftonline.com/common" + self._setup_mocks(mock_authority_class, authority) + + # Create app with BOTH thumbprints for AAD + sha1_thumbprint = "A1B2C3D4E5F6" + sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2" + app = ConfidentialClientApplication( + client_id="my_client_id", + client_credential={ + "private_key": self.test_private_key, + "thumbprint": sha1_thumbprint, + "thumbprint_sha256": sha256_thumbprint, + }, + authority=authority + ) + + # For AAD, should use SHA-256 when both are provided + self._verify_assertion_params( + mock_jwt_creator_class, + expected_algorithm='PS256', + expected_thumbprint_type='sha256' + ) + + def test_pem_with_both_thumbprints_adfs_uses_sha1( + self, mock_jwt_creator_class, mock_authority_class): + """Test that with both thumbprints, ADFS authority uses SHA-1""" + authority = "https://adfs.contoso.com/adfs" + self._setup_mocks(mock_authority_class, authority) + + # Create app with BOTH thumbprints for ADFS + sha1_thumbprint = "A1B2C3D4E5F6" + sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2" + app = ConfidentialClientApplication( + client_id="my_client_id", + client_credential={ + "private_key": self.test_private_key, + "thumbprint": sha1_thumbprint, + "thumbprint_sha256": sha256_thumbprint, + }, + authority=authority + ) + + # For ADFS, should use SHA-1 when both are provided + self._verify_assertion_params( + mock_jwt_creator_class, + expected_algorithm='RS256', + expected_thumbprint_type='sha1', + expected_thumbprint_value=sha1_thumbprint + ) + + def test_pem_with_both_thumbprints_b2c_uses_sha256( + self, mock_jwt_creator_class, mock_authority_class): + """Test that with both thumbprints, B2C authority uses SHA-256""" + authority = "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi" + mock_authority = self._setup_mocks(mock_authority_class, authority) + + # Manually set _is_b2c to True for this B2C authority + mock_authority._is_b2c = True + + # Create app with BOTH thumbprints for B2C + sha1_thumbprint = "A1B2C3D4E5F6" + sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2" + app = ConfidentialClientApplication( + client_id="my_client_id", + client_credential={ + "private_key": self.test_private_key, + "thumbprint": sha1_thumbprint, + "thumbprint_sha256": sha256_thumbprint, + }, + authority=authority + ) + + # For B2C, should use SHA-256 when both are provided + self._verify_assertion_params( + mock_jwt_creator_class, + expected_algorithm='PS256', + expected_thumbprint_type='sha256' + ) + + def test_pem_with_both_thumbprints_ciam_uses_sha256( + self, mock_jwt_creator_class, mock_authority_class): + """Test that with both thumbprints, CIAM authority uses SHA-256""" + authority = "https://contoso.ciamlogin.com/contoso.onmicrosoft.com" + mock_authority = self._setup_mocks(mock_authority_class, authority) + + # Create app with BOTH thumbprints for CIAM + sha1_thumbprint = "A1B2C3D4E5F6" + sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2" + app = ConfidentialClientApplication( + client_id="my_client_id", + client_credential={ + "private_key": self.test_private_key, + "thumbprint": sha1_thumbprint, + "thumbprint_sha256": sha256_thumbprint, + }, + authority=authority + ) + + # For CIAM, should use SHA-256 when both are provided + self._verify_assertion_params( + mock_jwt_creator_class, + expected_algorithm='PS256', + expected_thumbprint_type='sha256' + ) + + def test_pem_with_both_thumbprints_generic_uses_sha1( + self, mock_jwt_creator_class, mock_authority_class): + """Test that with both thumbprints, generic authority uses SHA-1""" + authority = "https://custom.authority.com/tenant" + mock_authority = self._setup_mocks(mock_authority_class, authority) + + # Set up as a generic authority (not ADFS, not B2C, not in known hosts) + mock_authority.is_adfs = False + mock_authority._is_b2c = False + + # Create app with BOTH thumbprints for generic authority + sha1_thumbprint = "A1B2C3D4E5F6" + sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2" + app = ConfidentialClientApplication( + client_id="my_client_id", + client_credential={ + "private_key": self.test_private_key, + "thumbprint": sha1_thumbprint, + "thumbprint_sha256": sha256_thumbprint, + }, + authority=authority + ) + + # For generic authorities, should use SHA-1 when both are provided + self._verify_assertion_params( + mock_jwt_creator_class, + expected_algorithm='RS256', + expected_thumbprint_type='sha1', + expected_thumbprint_value=sha1_thumbprint + ) + if __name__ == "__main__": unittest.main() From dd436946baf76213c3bbf6375a695dc8b57c11d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:06:44 +0000 Subject: [PATCH 3/8] Address code review feedback - clarify comments and simplify logic Co-authored-by: bgavrilMS <12273384+bgavrilMS@users.noreply.github.com> --- msal/application.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/msal/application.py b/msal/application.py index 01582727..5f315d87 100644 --- a/msal/application.py +++ b/msal/application.py @@ -851,9 +851,9 @@ def _build_client(self, client_credential, authority, skip_regional_client=False ): # Then we treat the public_certificate value as PEM content headers["x5c"] = extract_certs(client_credential['public_certificate']) # Determine which thumbprint to use based on what's available and authority type - # Spec: If both thumbprints are provided: - # - Use SHA256 for AAD authorities (including B2C, CIAM) - # - Use SHA1 for ADFS and generic authorities + # Based on the feature requirement: + # - If both thumbprints are provided, use SHA256 for AAD authorities + # (including B2C, CIAM), and SHA1 for ADFS and generic authorities use_sha256 = False if sha256_thumbprint and sha1_thumbprint: # Both thumbprints provided - choose based on authority type @@ -869,11 +869,9 @@ def _build_client(self, client_credential, authority, skip_regional_client=False elif sha256_thumbprint: # Only SHA256 provided use_sha256 = True - elif sha1_thumbprint: - # Only SHA1 provided - use_sha256 = False else: - raise ValueError("You must provide either 'thumbprint' (SHA-1) or 'thumbprint_sha256' (SHA-256).") + # Only SHA1 provided or fallback + use_sha256 = False if use_sha256: assertion_params = { From ba84c3cdfb63822037b630c4798dcf965eb99efa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:09:41 +0000 Subject: [PATCH 4/8] Refactor: Use authority._is_b2c and extract test constants Co-authored-by: bgavrilMS <12273384+bgavrilMS@users.noreply.github.com> --- msal/application.py | 7 ++--- tests/test_optional_thumbprint.py | 52 +++++++++++++------------------ 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/msal/application.py b/msal/application.py index 5f315d87..d9e0ba57 100644 --- a/msal/application.py +++ b/msal/application.py @@ -858,12 +858,9 @@ def _build_client(self, client_credential, authority, skip_regional_client=False if sha256_thumbprint and sha1_thumbprint: # Both thumbprints provided - choose based on authority type # Use SHA256 for AAD (including B2C, CIAM), SHA1 for ADFS and generic - from .authority import WELL_KNOWN_AUTHORITY_HOSTS, WELL_KNOWN_B2C_HOSTS, _CIAM_DOMAIN_SUFFIX + from .authority import WELL_KNOWN_AUTHORITY_HOSTS is_known_aad = authority.instance in WELL_KNOWN_AUTHORITY_HOSTS - is_b2c_or_ciam = ( - authority.instance.endswith(_CIAM_DOMAIN_SUFFIX) or - any(authority.instance.endswith("." + d) for d in WELL_KNOWN_B2C_HOSTS) - ) + is_b2c_or_ciam = getattr(authority, '_is_b2c', False) # Use SHA256 for known AAD, B2C, or CIAM; SHA1 for ADFS and generic use_sha256 = (is_known_aad or is_b2c_or_ciam) and not authority.is_adfs elif sha256_thumbprint: diff --git a/tests/test_optional_thumbprint.py b/tests/test_optional_thumbprint.py index 996e008e..424d1e9a 100644 --- a/tests/test_optional_thumbprint.py +++ b/tests/test_optional_thumbprint.py @@ -21,6 +21,11 @@ class TestClientCredentialWithOptionalThumbprint(unittest.TestCase): BAMMC0V4YW1wbGUgQ0EwHhcNMjQwMTAxMDAwMDAwWhcNMjUwMTAxMDAwMDAwWjAW -----END CERTIFICATE-----""" + # Test thumbprint values + test_sha1_thumbprint = "A1B2C3D4E5F6" + test_sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2" + + def _setup_mocks(self, mock_authority_class, authority="https://login.microsoftonline.com/common"): """Helper to setup Authority mock""" # Setup Authority mock @@ -119,12 +124,11 @@ def test_pem_with_manual_thumbprint_uses_sha1( 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, + "thumbprint": self.test_sha1_thumbprint, # Note: NO public_certificate provided }, authority=authority @@ -135,7 +139,7 @@ def test_pem_with_manual_thumbprint_uses_sha1( mock_jwt_creator_class, expected_algorithm='RS256', expected_thumbprint_type='sha1', - expected_thumbprint_value=manual_thumbprint + expected_thumbprint_value=self.test_sha1_thumbprint ) def test_pem_with_both_uses_manual_thumbprint_as_sha1( @@ -145,12 +149,11 @@ def test_pem_with_both_uses_manual_thumbprint_as_sha1( 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, + "thumbprint": self.test_sha1_thumbprint, "public_certificate": self.test_certificate, }, authority=authority @@ -161,7 +164,7 @@ def test_pem_with_both_uses_manual_thumbprint_as_sha1( mock_jwt_creator_class, expected_algorithm='RS256', expected_thumbprint_type='sha1', - expected_thumbprint_value=manual_thumbprint, + expected_thumbprint_value=self.test_sha1_thumbprint, has_x5c=True # x5c should still be present ) @@ -217,12 +220,11 @@ def test_pem_with_thumbprint_sha256_only_uses_sha256( self._setup_mocks(mock_authority_class, authority) # Create app with only SHA256 thumbprint - sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2" app = ConfidentialClientApplication( client_id="my_client_id", client_credential={ "private_key": self.test_private_key, - "thumbprint_sha256": sha256_thumbprint, + "thumbprint_sha256": self.test_sha256_thumbprint, }, authority=authority ) @@ -241,14 +243,12 @@ def test_pem_with_both_thumbprints_aad_uses_sha256( self._setup_mocks(mock_authority_class, authority) # Create app with BOTH thumbprints for AAD - sha1_thumbprint = "A1B2C3D4E5F6" - sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2" app = ConfidentialClientApplication( client_id="my_client_id", client_credential={ "private_key": self.test_private_key, - "thumbprint": sha1_thumbprint, - "thumbprint_sha256": sha256_thumbprint, + "thumbprint": self.test_sha1_thumbprint, + "thumbprint_sha256": self.test_sha256_thumbprint, }, authority=authority ) @@ -267,14 +267,12 @@ def test_pem_with_both_thumbprints_adfs_uses_sha1( self._setup_mocks(mock_authority_class, authority) # Create app with BOTH thumbprints for ADFS - sha1_thumbprint = "A1B2C3D4E5F6" - sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2" app = ConfidentialClientApplication( client_id="my_client_id", client_credential={ "private_key": self.test_private_key, - "thumbprint": sha1_thumbprint, - "thumbprint_sha256": sha256_thumbprint, + "thumbprint": self.test_sha1_thumbprint, + "thumbprint_sha256": self.test_sha256_thumbprint, }, authority=authority ) @@ -284,7 +282,7 @@ def test_pem_with_both_thumbprints_adfs_uses_sha1( mock_jwt_creator_class, expected_algorithm='RS256', expected_thumbprint_type='sha1', - expected_thumbprint_value=sha1_thumbprint + expected_thumbprint_value=self.test_sha1_thumbprint ) def test_pem_with_both_thumbprints_b2c_uses_sha256( @@ -297,14 +295,12 @@ def test_pem_with_both_thumbprints_b2c_uses_sha256( mock_authority._is_b2c = True # Create app with BOTH thumbprints for B2C - sha1_thumbprint = "A1B2C3D4E5F6" - sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2" app = ConfidentialClientApplication( client_id="my_client_id", client_credential={ "private_key": self.test_private_key, - "thumbprint": sha1_thumbprint, - "thumbprint_sha256": sha256_thumbprint, + "thumbprint": self.test_sha1_thumbprint, + "thumbprint_sha256": self.test_sha256_thumbprint, }, authority=authority ) @@ -323,14 +319,12 @@ def test_pem_with_both_thumbprints_ciam_uses_sha256( mock_authority = self._setup_mocks(mock_authority_class, authority) # Create app with BOTH thumbprints for CIAM - sha1_thumbprint = "A1B2C3D4E5F6" - sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2" app = ConfidentialClientApplication( client_id="my_client_id", client_credential={ "private_key": self.test_private_key, - "thumbprint": sha1_thumbprint, - "thumbprint_sha256": sha256_thumbprint, + "thumbprint": self.test_sha1_thumbprint, + "thumbprint_sha256": self.test_sha256_thumbprint, }, authority=authority ) @@ -353,14 +347,12 @@ def test_pem_with_both_thumbprints_generic_uses_sha1( mock_authority._is_b2c = False # Create app with BOTH thumbprints for generic authority - sha1_thumbprint = "A1B2C3D4E5F6" - sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2" app = ConfidentialClientApplication( client_id="my_client_id", client_credential={ "private_key": self.test_private_key, - "thumbprint": sha1_thumbprint, - "thumbprint_sha256": sha256_thumbprint, + "thumbprint": self.test_sha1_thumbprint, + "thumbprint_sha256": self.test_sha256_thumbprint, }, authority=authority ) @@ -370,7 +362,7 @@ def test_pem_with_both_thumbprints_generic_uses_sha1( mock_jwt_creator_class, expected_algorithm='RS256', expected_thumbprint_type='sha1', - expected_thumbprint_value=sha1_thumbprint + expected_thumbprint_value=self.test_sha1_thumbprint ) From 938db072e5226914868c0c2643da5529aa78b6d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:11:06 +0000 Subject: [PATCH 5/8] Add thumbprint value verification to SHA256 tests Co-authored-by: bgavrilMS <12273384+bgavrilMS@users.noreply.github.com> --- tests/test_optional_thumbprint.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/test_optional_thumbprint.py b/tests/test_optional_thumbprint.py index 424d1e9a..d90edb7d 100644 --- a/tests/test_optional_thumbprint.py +++ b/tests/test_optional_thumbprint.py @@ -76,6 +76,8 @@ def _verify_assertion_params(self, mock_jwt_creator_class, expected_algorithm, if expected_thumbprint_type == 'sha256': self.assertIn('sha256_thumbprint', call_args[1]) self.assertNotIn('sha1_thumbprint', call_args[1]) + if expected_thumbprint_value: + self.assertEqual(call_args[1]['sha256_thumbprint'], expected_thumbprint_value) elif expected_thumbprint_type == 'sha1': self.assertIn('sha1_thumbprint', call_args[1]) self.assertNotIn('sha256_thumbprint', call_args[1]) @@ -233,7 +235,8 @@ def test_pem_with_thumbprint_sha256_only_uses_sha256( self._verify_assertion_params( mock_jwt_creator_class, expected_algorithm='PS256', - expected_thumbprint_type='sha256' + expected_thumbprint_type='sha256', + expected_thumbprint_value=self.test_sha256_thumbprint ) def test_pem_with_both_thumbprints_aad_uses_sha256( @@ -257,7 +260,8 @@ def test_pem_with_both_thumbprints_aad_uses_sha256( self._verify_assertion_params( mock_jwt_creator_class, expected_algorithm='PS256', - expected_thumbprint_type='sha256' + expected_thumbprint_type='sha256', + expected_thumbprint_value=self.test_sha256_thumbprint ) def test_pem_with_both_thumbprints_adfs_uses_sha1( @@ -309,7 +313,8 @@ def test_pem_with_both_thumbprints_b2c_uses_sha256( self._verify_assertion_params( mock_jwt_creator_class, expected_algorithm='PS256', - expected_thumbprint_type='sha256' + expected_thumbprint_type='sha256', + expected_thumbprint_value=self.test_sha256_thumbprint ) def test_pem_with_both_thumbprints_ciam_uses_sha256( @@ -333,7 +338,8 @@ def test_pem_with_both_thumbprints_ciam_uses_sha256( self._verify_assertion_params( mock_jwt_creator_class, expected_algorithm='PS256', - expected_thumbprint_type='sha256' + expected_thumbprint_type='sha256', + expected_thumbprint_value=self.test_sha256_thumbprint ) def test_pem_with_both_thumbprints_generic_uses_sha1( From 81240fb927b1dd24de459dfba007b8966fa7dc4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 13:12:33 +0000 Subject: [PATCH 6/8] Polish: Improve error message clarity and comment placement Co-authored-by: bgavrilMS <12273384+bgavrilMS@users.noreply.github.com> --- msal/application.py | 4 ++-- tests/test_optional_thumbprint.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/msal/application.py b/msal/application.py index d9e0ba57..cb0ff7fe 100644 --- a/msal/application.py +++ b/msal/application.py @@ -840,8 +840,8 @@ def _build_client(self, client_credential, authority, skip_regional_client=False _extract_cert_and_thumbprints(cert)) else: raise ValueError( - "You must provide either 'thumbprint', 'thumbprint_sha256', or 'public_certificate' " - "from which the thumbprint can be calculated.") + "You must provide 'thumbprint' (SHA-1), 'thumbprint_sha256' (SHA-256), " + "or 'public_certificate' from which the thumbprint can be calculated.") else: raise ValueError( "client_credential needs to follow this format " diff --git a/tests/test_optional_thumbprint.py b/tests/test_optional_thumbprint.py index d90edb7d..85538c12 100644 --- a/tests/test_optional_thumbprint.py +++ b/tests/test_optional_thumbprint.py @@ -294,9 +294,7 @@ def test_pem_with_both_thumbprints_b2c_uses_sha256( """Test that with both thumbprints, B2C authority uses SHA-256""" authority = "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi" mock_authority = self._setup_mocks(mock_authority_class, authority) - - # Manually set _is_b2c to True for this B2C authority - mock_authority._is_b2c = True + mock_authority._is_b2c = True # Manually set _is_b2c to True for this B2C authority # Create app with BOTH thumbprints for B2C app = ConfidentialClientApplication( From 0a8d2bef816e861244a62e977748813d3767899d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:12:30 +0000 Subject: [PATCH 7/8] Refactor: Use _is_oidc flag for proper authority classification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add _is_oidc flag to Authority class to distinguish OIDC generic authorities - Simplify thumbprint selection logic: use SHA256 for all except ADFS and OIDC - Authority classification now: * ADFS: authority.is_adfs → SHA1 * B2C: authority._is_b2c (not OIDC) → SHA256 * CIAM: authority._is_b2c (not OIDC) → SHA256 * OIDC generic: authority._is_oidc → SHA1 * AAD: everything else → SHA256 - Update tests to reflect new classification - Add test for unknown AAD authority (sovereign cloud) Co-authored-by: bgavrilMS <12273384+bgavrilMS@users.noreply.github.com> --- msal/application.py | 19 +++++------ msal/authority.py | 2 ++ tests/test_optional_thumbprint.py | 52 ++++++++++++++++++++++++++----- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/msal/application.py b/msal/application.py index cb0ff7fe..39dd2bb5 100644 --- a/msal/application.py +++ b/msal/application.py @@ -851,18 +851,19 @@ def _build_client(self, client_credential, authority, skip_regional_client=False ): # Then we treat the public_certificate value as PEM content headers["x5c"] = extract_certs(client_credential['public_certificate']) # Determine which thumbprint to use based on what's available and authority type - # Based on the feature requirement: - # - If both thumbprints are provided, use SHA256 for AAD authorities - # (including B2C, CIAM), and SHA1 for ADFS and generic authorities + # Authority classification: + # - ADFS: authority.is_adfs + # - B2C: authority._is_b2c (and not OIDC) + # - CIAM: authority._is_b2c (and not OIDC) + # - OIDC generic: authority._is_oidc + # - AAD: everything else + # Use SHA256 for AAD, B2C, CIAM; use SHA1 for ADFS and OIDC generic use_sha256 = False if sha256_thumbprint and sha1_thumbprint: # Both thumbprints provided - choose based on authority type - # Use SHA256 for AAD (including B2C, CIAM), SHA1 for ADFS and generic - from .authority import WELL_KNOWN_AUTHORITY_HOSTS - is_known_aad = authority.instance in WELL_KNOWN_AUTHORITY_HOSTS - is_b2c_or_ciam = getattr(authority, '_is_b2c', False) - # Use SHA256 for known AAD, B2C, or CIAM; SHA1 for ADFS and generic - use_sha256 = (is_known_aad or is_b2c_or_ciam) and not authority.is_adfs + is_oidc = getattr(authority, '_is_oidc', False) + # Use SHA1 for ADFS and OIDC generic; SHA256 for everything else (AAD, B2C, CIAM) + use_sha256 = not authority.is_adfs and not is_oidc elif sha256_thumbprint: # Only SHA256 provided use_sha256 = True diff --git a/msal/authority.py b/msal/authority.py index 96204966..87f1ae59 100644 --- a/msal/authority.py +++ b/msal/authority.py @@ -107,6 +107,7 @@ def _initialize_oidc_authority(self, oidc_authority_url): self._is_b2c = True # Not exactly true, but # OIDC Authority was designed for CIAM which is the next gen of B2C. # Besides, application.py uses this to bypass broker. + self._is_oidc = True # Track that this is a generic OIDC authority self._is_known_to_developer = True # Not really relevant, but application.py uses this to bypass authority validation return oidc_authority_url + "/.well-known/openid-configuration" @@ -126,6 +127,7 @@ def _initialize_entra_authority( self._is_b2c = any( self.instance.endswith("." + d) for d in WELL_KNOWN_B2C_HOSTS ) or (len(parts) == 3 and parts[2].lower().startswith("b2c_")) + self._is_oidc = False # This is not a generic OIDC authority self._is_known_to_developer = self.is_adfs or self._is_b2c or not validate_authority is_known_to_microsoft = self.instance in WELL_KNOWN_AUTHORITY_HOSTS instance_discovery_endpoint = 'https://{}/common/discovery/instance'.format( # Note: This URL seemingly returns V1 endpoint only diff --git a/tests/test_optional_thumbprint.py b/tests/test_optional_thumbprint.py index 85538c12..e02ce05f 100644 --- a/tests/test_optional_thumbprint.py +++ b/tests/test_optional_thumbprint.py @@ -97,7 +97,8 @@ 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) + mock_authority = self._setup_mocks(mock_authority_class, authority) + mock_authority._is_oidc = False # AAD is not OIDC generic self._setup_certificate_mocks(mock_extract, mock_load_cert) # Create app with certificate credential WITHOUT thumbprint @@ -243,7 +244,8 @@ def test_pem_with_both_thumbprints_aad_uses_sha256( self, mock_jwt_creator_class, mock_authority_class): """Test that with both thumbprints, AAD authority uses SHA-256""" authority = "https://login.microsoftonline.com/common" - self._setup_mocks(mock_authority_class, authority) + mock_authority = self._setup_mocks(mock_authority_class, authority) + mock_authority._is_oidc = False # AAD is not OIDC generic # Create app with BOTH thumbprints for AAD app = ConfidentialClientApplication( @@ -295,6 +297,7 @@ def test_pem_with_both_thumbprints_b2c_uses_sha256( authority = "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi" mock_authority = self._setup_mocks(mock_authority_class, authority) mock_authority._is_b2c = True # Manually set _is_b2c to True for this B2C authority + mock_authority._is_oidc = False # B2C is not OIDC generic # Create app with BOTH thumbprints for B2C app = ConfidentialClientApplication( @@ -320,6 +323,8 @@ def test_pem_with_both_thumbprints_ciam_uses_sha256( """Test that with both thumbprints, CIAM authority uses SHA-256""" authority = "https://contoso.ciamlogin.com/contoso.onmicrosoft.com" mock_authority = self._setup_mocks(mock_authority_class, authority) + mock_authority._is_b2c = True # CIAM sets _is_b2c to True + mock_authority._is_oidc = False # CIAM is not OIDC generic # Create app with BOTH thumbprints for CIAM app = ConfidentialClientApplication( @@ -342,15 +347,16 @@ def test_pem_with_both_thumbprints_ciam_uses_sha256( def test_pem_with_both_thumbprints_generic_uses_sha1( self, mock_jwt_creator_class, mock_authority_class): - """Test that with both thumbprints, generic authority uses SHA-1""" - authority = "https://custom.authority.com/tenant" + """Test that with both thumbprints, OIDC generic authority uses SHA-1""" + authority = "https://custom.oidc.authority.com/tenant" mock_authority = self._setup_mocks(mock_authority_class, authority) - # Set up as a generic authority (not ADFS, not B2C, not in known hosts) + # Set up as an OIDC generic authority mock_authority.is_adfs = False - mock_authority._is_b2c = False + mock_authority._is_b2c = True # OIDC sets this but it's not truly B2C + mock_authority._is_oidc = True # This distinguishes OIDC from B2C/CIAM - # Create app with BOTH thumbprints for generic authority + # Create app with BOTH thumbprints for OIDC generic authority app = ConfidentialClientApplication( client_id="my_client_id", client_credential={ @@ -361,7 +367,7 @@ def test_pem_with_both_thumbprints_generic_uses_sha1( authority=authority ) - # For generic authorities, should use SHA-1 when both are provided + # For OIDC generic authorities, should use SHA-1 when both are provided self._verify_assertion_params( mock_jwt_creator_class, expected_algorithm='RS256', @@ -369,6 +375,36 @@ def test_pem_with_both_thumbprints_generic_uses_sha1( expected_thumbprint_value=self.test_sha1_thumbprint ) + def test_pem_with_both_thumbprints_unknown_aad_uses_sha256( + self, mock_jwt_creator_class, mock_authority_class): + """Test that with both thumbprints, unknown AAD authority (e.g., sovereign cloud) uses SHA-256""" + authority = "https://login.microsoftonline.de/tenant" # Example of sovereign cloud not in known list + mock_authority = self._setup_mocks(mock_authority_class, authority) + + # Set up as an AAD authority (not ADFS, not B2C, not OIDC) + mock_authority.is_adfs = False + mock_authority._is_b2c = False + mock_authority._is_oidc = False + + # Create app with BOTH thumbprints for unknown AAD authority + app = ConfidentialClientApplication( + client_id="my_client_id", + client_credential={ + "private_key": self.test_private_key, + "thumbprint": self.test_sha1_thumbprint, + "thumbprint_sha256": self.test_sha256_thumbprint, + }, + authority=authority + ) + + # For AAD authorities (even unknown ones), should use SHA-256 when both are provided + self._verify_assertion_params( + mock_jwt_creator_class, + expected_algorithm='PS256', + expected_thumbprint_type='sha256', + expected_thumbprint_value=self.test_sha256_thumbprint + ) + if __name__ == "__main__": unittest.main() From a9fb14e918167f6fa79825bceb9e1ce7c217eb58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 16:16:36 +0000 Subject: [PATCH 8/8] Add dSTS support - use SHA1 for dSTS authorities - Update comments to explicitly mention dSTS as part of OIDC generic - Add test for dSTS authority to verify SHA1 is used - dSTS is already handled correctly as it's treated as OIDC authority - All 13 tests passing Co-authored-by: bgavrilMS <12273384+bgavrilMS@users.noreply.github.com> --- msal/application.py | 6 +++--- tests/test_optional_thumbprint.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/msal/application.py b/msal/application.py index 39dd2bb5..d270ecc7 100644 --- a/msal/application.py +++ b/msal/application.py @@ -855,14 +855,14 @@ def _build_client(self, client_credential, authority, skip_regional_client=False # - ADFS: authority.is_adfs # - B2C: authority._is_b2c (and not OIDC) # - CIAM: authority._is_b2c (and not OIDC) - # - OIDC generic: authority._is_oidc + # - OIDC generic: authority._is_oidc (includes dSTS) # - AAD: everything else - # Use SHA256 for AAD, B2C, CIAM; use SHA1 for ADFS and OIDC generic + # Use SHA256 for AAD, B2C, CIAM; use SHA1 for ADFS, OIDC generic, and dSTS use_sha256 = False if sha256_thumbprint and sha1_thumbprint: # Both thumbprints provided - choose based on authority type is_oidc = getattr(authority, '_is_oidc', False) - # Use SHA1 for ADFS and OIDC generic; SHA256 for everything else (AAD, B2C, CIAM) + # Use SHA1 for ADFS, OIDC generic (including dSTS); SHA256 for everything else (AAD, B2C, CIAM) use_sha256 = not authority.is_adfs and not is_oidc elif sha256_thumbprint: # Only SHA256 provided diff --git a/tests/test_optional_thumbprint.py b/tests/test_optional_thumbprint.py index e02ce05f..e3e053d1 100644 --- a/tests/test_optional_thumbprint.py +++ b/tests/test_optional_thumbprint.py @@ -375,6 +375,36 @@ def test_pem_with_both_thumbprints_generic_uses_sha1( expected_thumbprint_value=self.test_sha1_thumbprint ) + def test_pem_with_both_thumbprints_dsts_uses_sha1( + self, mock_jwt_creator_class, mock_authority_class): + """Test that with both thumbprints, dSTS authority uses SHA-1""" + authority = "https://test-instance1-dsts.dsts.core.azure-test.net/dstsv2/common" + mock_authority = self._setup_mocks(mock_authority_class, authority) + + # Set up as a dSTS authority (dSTS is treated as OIDC) + mock_authority.is_adfs = False + mock_authority._is_b2c = True # OIDC sets this but it's not truly B2C + mock_authority._is_oidc = True # dSTS is treated as OIDC generic + + # Create app with BOTH thumbprints for dSTS authority + app = ConfidentialClientApplication( + client_id="my_client_id", + client_credential={ + "private_key": self.test_private_key, + "thumbprint": self.test_sha1_thumbprint, + "thumbprint_sha256": self.test_sha256_thumbprint, + }, + authority=authority + ) + + # For dSTS authorities, should use SHA-1 when both are provided + self._verify_assertion_params( + mock_jwt_creator_class, + expected_algorithm='RS256', + expected_thumbprint_type='sha1', + expected_thumbprint_value=self.test_sha1_thumbprint + ) + def test_pem_with_both_thumbprints_unknown_aad_uses_sha256( self, mock_jwt_creator_class, mock_authority_class): """Test that with both thumbprints, unknown AAD authority (e.g., sovereign cloud) uses SHA-256"""