Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions msal/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
Expand Down Expand Up @@ -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
Expand All @@ -836,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' 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 "
Expand All @@ -846,13 +850,31 @@ 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
# 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
# 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
elif sha256_thumbprint:
# Only SHA256 provided
use_sha256 = True
else:
# Only SHA1 provided or fallback
use_sha256 = False

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,
}
Expand Down
171 changes: 165 additions & 6 deletions tests/test_optional_thumbprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -71,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])
Expand Down Expand Up @@ -119,12 +126,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
Expand All @@ -135,7 +141,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(
Expand All @@ -145,12 +151,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
Expand All @@ -161,7 +166,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
)

Expand Down Expand Up @@ -210,6 +215,160 @@ 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
app = ConfidentialClientApplication(
client_id="my_client_id",
client_credential={
"private_key": self.test_private_key,
"thumbprint_sha256": self.test_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',
expected_thumbprint_value=self.test_sha256_thumbprint
)

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
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, 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
)

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
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 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=self.test_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)
mock_authority._is_b2c = True # Manually set _is_b2c to True for this B2C authority

# Create app with BOTH thumbprints for B2C
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 B2C, 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
)

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
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 CIAM, 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
)

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
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 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=self.test_sha1_thumbprint
)


if __name__ == "__main__":
unittest.main()
Loading