Skip to content

Commit 77fdd50

Browse files
CopilotbgavrilMS
andcommitted
Add support for SHA256 certificate thumbprint with authority-based selection
Co-authored-by: bgavrilMS <12273384+bgavrilMS@users.noreply.github.com>
1 parent 55483b2 commit 77fdd50

File tree

2 files changed

+199
-9
lines changed

2 files changed

+199
-9
lines changed

msal/application.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,10 @@ def __init__(
300300
"Changed in version 1.35.0, if thumbprint is absent"
301301
"and a public_certificate is present, MSAL will"
302302
"automatically calculate an SHA-256 thumbprint instead.",
303+
"thumbprint_sha256": "An SHA-256 thumbprint (Added in version 1.35.0). "
304+
"If both thumbprint and thumbprint_sha256 are provided, "
305+
"SHA-256 is used for AAD authorities (including B2C, CIAM), "
306+
"and SHA-1 is used for ADFS and generic authorities.",
303307
"passphrase": "Needed if the private_key is encrypted (Added in version 1.6.0)",
304308
"public_certificate": "...-----BEGIN CERTIFICATE-----...", # Needed if you use Subject Name/Issuer auth. Added in version 0.5.0.
305309
}
@@ -823,10 +827,10 @@ def _build_client(self, client_credential, authority, skip_regional_client=False
823827
)
824828

825829
# Determine thumbprints based on what's provided
826-
if client_credential.get("thumbprint"):
827-
# User provided a thumbprint - use it as SHA-1 (legacy/manual approach)
828-
sha1_thumbprint = client_credential["thumbprint"]
829-
sha256_thumbprint = None
830+
if client_credential.get("thumbprint") or client_credential.get("thumbprint_sha256"):
831+
# User provided one or both thumbprints - use them as-is
832+
sha1_thumbprint = client_credential.get("thumbprint")
833+
sha256_thumbprint = client_credential.get("thumbprint_sha256")
830834
elif isinstance(client_credential.get('public_certificate'), str):
831835
# No thumbprint provided, but we have a certificate to calculate thumbprints
832836
from cryptography import x509
@@ -836,7 +840,7 @@ def _build_client(self, client_credential, authority, skip_regional_client=False
836840
_extract_cert_and_thumbprints(cert))
837841
else:
838842
raise ValueError(
839-
"You must provide either 'thumbprint' or 'public_certificate' "
843+
"You must provide either 'thumbprint', 'thumbprint_sha256', or 'public_certificate' "
840844
"from which the thumbprint can be calculated.")
841845
else:
842846
raise ValueError(
@@ -846,13 +850,36 @@ def _build_client(self, client_credential, authority, skip_regional_client=False
846850
and isinstance(client_credential.get('public_certificate'), str)
847851
): # Then we treat the public_certificate value as PEM content
848852
headers["x5c"] = extract_certs(client_credential['public_certificate'])
849-
if sha256_thumbprint and not authority.is_adfs:
853+
# Determine which thumbprint to use based on what's available and authority type
854+
# Spec: If both thumbprints are provided:
855+
# - Use SHA256 for AAD authorities (including B2C, CIAM)
856+
# - Use SHA1 for ADFS and generic authorities
857+
use_sha256 = False
858+
if sha256_thumbprint and sha1_thumbprint:
859+
# Both thumbprints provided - choose based on authority type
860+
# Use SHA256 for AAD (including B2C, CIAM), SHA1 for ADFS and generic
861+
from .authority import WELL_KNOWN_AUTHORITY_HOSTS, WELL_KNOWN_B2C_HOSTS, _CIAM_DOMAIN_SUFFIX
862+
is_known_aad = authority.instance in WELL_KNOWN_AUTHORITY_HOSTS
863+
is_b2c_or_ciam = (
864+
authority.instance.endswith(_CIAM_DOMAIN_SUFFIX) or
865+
any(authority.instance.endswith("." + d) for d in WELL_KNOWN_B2C_HOSTS)
866+
)
867+
# Use SHA256 for known AAD, B2C, or CIAM; SHA1 for ADFS and generic
868+
use_sha256 = (is_known_aad or is_b2c_or_ciam) and not authority.is_adfs
869+
elif sha256_thumbprint:
870+
# Only SHA256 provided
871+
use_sha256 = True
872+
elif sha1_thumbprint:
873+
# Only SHA1 provided
874+
use_sha256 = False
875+
else:
876+
raise ValueError("You must provide either 'thumbprint' (SHA-1) or 'thumbprint_sha256' (SHA-256).")
877+
878+
if use_sha256:
850879
assertion_params = {
851880
"algorithm": "PS256", "sha256_thumbprint": sha256_thumbprint,
852881
}
853-
else: # Fall back
854-
if not sha1_thumbprint:
855-
raise ValueError("You shall provide a thumbprint in SHA1.")
882+
else:
856883
assertion_params = {
857884
"algorithm": "RS256", "sha1_thumbprint": sha1_thumbprint,
858885
}

tests/test_optional_thumbprint.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,169 @@ def test_pem_with_neither_raises_error(self, mock_jwt_creator_class, mock_author
210210
self.assertIn("thumbprint", str(context.exception).lower())
211211
self.assertIn("public_certificate", str(context.exception).lower())
212212

213+
def test_pem_with_thumbprint_sha256_only_uses_sha256(
214+
self, mock_jwt_creator_class, mock_authority_class):
215+
"""Test that providing only thumbprint_sha256 uses SHA-256"""
216+
authority = "https://login.microsoftonline.com/common"
217+
self._setup_mocks(mock_authority_class, authority)
218+
219+
# Create app with only SHA256 thumbprint
220+
sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"
221+
app = ConfidentialClientApplication(
222+
client_id="my_client_id",
223+
client_credential={
224+
"private_key": self.test_private_key,
225+
"thumbprint_sha256": sha256_thumbprint,
226+
},
227+
authority=authority
228+
)
229+
230+
# Verify SHA-256 with PS256 algorithm is used
231+
self._verify_assertion_params(
232+
mock_jwt_creator_class,
233+
expected_algorithm='PS256',
234+
expected_thumbprint_type='sha256'
235+
)
236+
237+
def test_pem_with_both_thumbprints_aad_uses_sha256(
238+
self, mock_jwt_creator_class, mock_authority_class):
239+
"""Test that with both thumbprints, AAD authority uses SHA-256"""
240+
authority = "https://login.microsoftonline.com/common"
241+
self._setup_mocks(mock_authority_class, authority)
242+
243+
# Create app with BOTH thumbprints for AAD
244+
sha1_thumbprint = "A1B2C3D4E5F6"
245+
sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"
246+
app = ConfidentialClientApplication(
247+
client_id="my_client_id",
248+
client_credential={
249+
"private_key": self.test_private_key,
250+
"thumbprint": sha1_thumbprint,
251+
"thumbprint_sha256": sha256_thumbprint,
252+
},
253+
authority=authority
254+
)
255+
256+
# For AAD, should use SHA-256 when both are provided
257+
self._verify_assertion_params(
258+
mock_jwt_creator_class,
259+
expected_algorithm='PS256',
260+
expected_thumbprint_type='sha256'
261+
)
262+
263+
def test_pem_with_both_thumbprints_adfs_uses_sha1(
264+
self, mock_jwt_creator_class, mock_authority_class):
265+
"""Test that with both thumbprints, ADFS authority uses SHA-1"""
266+
authority = "https://adfs.contoso.com/adfs"
267+
self._setup_mocks(mock_authority_class, authority)
268+
269+
# Create app with BOTH thumbprints for ADFS
270+
sha1_thumbprint = "A1B2C3D4E5F6"
271+
sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"
272+
app = ConfidentialClientApplication(
273+
client_id="my_client_id",
274+
client_credential={
275+
"private_key": self.test_private_key,
276+
"thumbprint": sha1_thumbprint,
277+
"thumbprint_sha256": sha256_thumbprint,
278+
},
279+
authority=authority
280+
)
281+
282+
# For ADFS, should use SHA-1 when both are provided
283+
self._verify_assertion_params(
284+
mock_jwt_creator_class,
285+
expected_algorithm='RS256',
286+
expected_thumbprint_type='sha1',
287+
expected_thumbprint_value=sha1_thumbprint
288+
)
289+
290+
def test_pem_with_both_thumbprints_b2c_uses_sha256(
291+
self, mock_jwt_creator_class, mock_authority_class):
292+
"""Test that with both thumbprints, B2C authority uses SHA-256"""
293+
authority = "https://contoso.b2clogin.com/contoso.onmicrosoft.com/B2C_1_susi"
294+
mock_authority = self._setup_mocks(mock_authority_class, authority)
295+
296+
# Manually set _is_b2c to True for this B2C authority
297+
mock_authority._is_b2c = True
298+
299+
# Create app with BOTH thumbprints for B2C
300+
sha1_thumbprint = "A1B2C3D4E5F6"
301+
sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"
302+
app = ConfidentialClientApplication(
303+
client_id="my_client_id",
304+
client_credential={
305+
"private_key": self.test_private_key,
306+
"thumbprint": sha1_thumbprint,
307+
"thumbprint_sha256": sha256_thumbprint,
308+
},
309+
authority=authority
310+
)
311+
312+
# For B2C, should use SHA-256 when both are provided
313+
self._verify_assertion_params(
314+
mock_jwt_creator_class,
315+
expected_algorithm='PS256',
316+
expected_thumbprint_type='sha256'
317+
)
318+
319+
def test_pem_with_both_thumbprints_ciam_uses_sha256(
320+
self, mock_jwt_creator_class, mock_authority_class):
321+
"""Test that with both thumbprints, CIAM authority uses SHA-256"""
322+
authority = "https://contoso.ciamlogin.com/contoso.onmicrosoft.com"
323+
mock_authority = self._setup_mocks(mock_authority_class, authority)
324+
325+
# Create app with BOTH thumbprints for CIAM
326+
sha1_thumbprint = "A1B2C3D4E5F6"
327+
sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"
328+
app = ConfidentialClientApplication(
329+
client_id="my_client_id",
330+
client_credential={
331+
"private_key": self.test_private_key,
332+
"thumbprint": sha1_thumbprint,
333+
"thumbprint_sha256": sha256_thumbprint,
334+
},
335+
authority=authority
336+
)
337+
338+
# For CIAM, should use SHA-256 when both are provided
339+
self._verify_assertion_params(
340+
mock_jwt_creator_class,
341+
expected_algorithm='PS256',
342+
expected_thumbprint_type='sha256'
343+
)
344+
345+
def test_pem_with_both_thumbprints_generic_uses_sha1(
346+
self, mock_jwt_creator_class, mock_authority_class):
347+
"""Test that with both thumbprints, generic authority uses SHA-1"""
348+
authority = "https://custom.authority.com/tenant"
349+
mock_authority = self._setup_mocks(mock_authority_class, authority)
350+
351+
# Set up as a generic authority (not ADFS, not B2C, not in known hosts)
352+
mock_authority.is_adfs = False
353+
mock_authority._is_b2c = False
354+
355+
# Create app with BOTH thumbprints for generic authority
356+
sha1_thumbprint = "A1B2C3D4E5F6"
357+
sha256_thumbprint = "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"
358+
app = ConfidentialClientApplication(
359+
client_id="my_client_id",
360+
client_credential={
361+
"private_key": self.test_private_key,
362+
"thumbprint": sha1_thumbprint,
363+
"thumbprint_sha256": sha256_thumbprint,
364+
},
365+
authority=authority
366+
)
367+
368+
# For generic authorities, should use SHA-1 when both are provided
369+
self._verify_assertion_params(
370+
mock_jwt_creator_class,
371+
expected_algorithm='RS256',
372+
expected_thumbprint_type='sha1',
373+
expected_thumbprint_value=sha1_thumbprint
374+
)
375+
213376

214377
if __name__ == "__main__":
215378
unittest.main()

0 commit comments

Comments
 (0)