@@ -66,24 +66,32 @@ def _str2bytes(raw):
6666 except :
6767 return raw
6868
69+ def _extract_cert_and_thumbprints (cert ):
70+ # Cert concepts https://security.stackexchange.com/a/226758/125264
71+ from cryptography .hazmat .primitives import hashes , serialization
72+ cert_pem = cert .public_bytes ( # Requires cryptography 1.0+
73+ encoding = serialization .Encoding .PEM ).decode ()
74+ x5c = [
75+ '\n ' .join (
76+ cert_pem .splitlines ()
77+ [1 :- 1 ] # Strip the "--- header ---" and "--- footer ---"
78+ )
79+ ]
80+ # https://cryptography.io/en/latest/x509/reference/#x-509-certificate-object - Requires cryptography 0.7+
81+ sha256_thumbprint = cert .fingerprint (hashes .SHA256 ()).hex ()
82+ sha1_thumbprint = cert .fingerprint (hashes .SHA1 ()).hex () # CodeQL [SM02167] for legacy support such as ADFS
83+ return sha256_thumbprint , sha1_thumbprint , x5c
6984
7085def _parse_pfx (pfx_path , passphrase_bytes ):
7186 # Cert concepts https://security.stackexchange.com/a/226758/125264
72- from cryptography .hazmat .primitives import hashes , serialization
7387 from cryptography .hazmat .primitives .serialization import pkcs12
7488 with open (pfx_path , 'rb' ) as f :
7589 private_key , cert , _ = pkcs12 .load_key_and_certificates ( # cryptography 2.5+
7690 # https://cryptography.io/en/latest/hazmat/primitives/asymmetric/serialization/#cryptography.hazmat.primitives.serialization.pkcs12.load_key_and_certificates
7791 f .read (), passphrase_bytes )
7892 if not (private_key and cert ):
7993 raise ValueError ("Your PFX file shall contain both private key and cert" )
80- cert_pem = cert .public_bytes (encoding = serialization .Encoding .PEM ).decode () # cryptography 1.0+
81- x5c = [
82- '\n ' .join (cert_pem .splitlines ()[1 :- 1 ]) # Strip the "--- header ---" and "--- footer ---"
83- ]
84- sha256_thumbprint = cert .fingerprint (hashes .SHA256 ()).hex () # cryptography 0.7+
85- sha1_thumbprint = cert .fingerprint (hashes .SHA1 ()).hex () # cryptography 0.7+
86- # https://cryptography.io/en/latest/x509/reference/#x-509-certificate-object
94+ sha256_thumbprint , sha1_thumbprint , x5c = _extract_cert_and_thumbprints (cert )
8795 return private_key , sha256_thumbprint , sha1_thumbprint , x5c
8896
8997
@@ -280,12 +288,20 @@ def __init__(
280288
281289 .. admonition:: Support using a certificate in X.509 (.pem) format
282290
291+ Deprecated because it uses SHA-1 thumbprint,
292+ unless you are still using ADFS which supports SHA-1 thumbprint only.
293+ Please use the .pfx option documented later in this page.
294+
283295 Feed in a dict in this form::
284296
285297 {
286298 "private_key": "...-----BEGIN PRIVATE KEY-----... in PEM format",
287- "thumbprint": "A1B2C3D4E5F6...",
288- "passphrase": "Passphrase if the private_key is encrypted (Optional. Added in version 1.6.0)",
299+ "thumbprint": "An SHA-1 thumbprint such as A1B2C3D4E5F6..."
300+ "Changed in version 1.35.0, if thumbprint is absent"
301+ "and a public_certificate is present, MSAL will"
302+ "automatically calculate an SHA-256 thumbprint instead.",
303+ "passphrase": "Needed if the private_key is encrypted (Added in version 1.6.0)",
304+ "public_certificate": "...-----BEGIN CERTIFICATE-----...", # Needed if you use Subject Name/Issuer auth. Added in version 0.5.0.
289305 }
290306
291307 MSAL Python requires a "private_key" in PEM format.
@@ -296,25 +312,11 @@ def __init__(
296312 The thumbprint is available in your app's registration in Azure Portal.
297313 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>`_.
298314
299- .. admonition:: Support Subject Name/Issuer Auth with a cert in .pem
300-
301- `Subject Name/Issuer Auth
302- <https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/60>`_
303- is an approach to allow easier certificate rotation.
304-
305- *Added in version 0.5.0*::
306-
307- {
308- "private_key": "...-----BEGIN PRIVATE KEY-----... in PEM format",
309- "thumbprint": "A1B2C3D4E5F6...",
310- "public_certificate": "...-----BEGIN CERTIFICATE-----...",
311- "passphrase": "Passphrase if the private_key is encrypted (Optional. Added in version 1.6.0)",
312- }
313-
314315 ``public_certificate`` (optional) is public key certificate
315- which will be sent through 'x5c' JWT header only for
316- subject name and issuer authentication to support cert auto rolls.
317-
316+ which will be sent through 'x5c' JWT header.
317+ This is useful when you use `Subject Name/Issuer Authentication
318+ <https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/60>`_
319+ which is an approach to allow easier certificate rotation.
318320 Per `specs <https://tools.ietf.org/html/rfc7515#section-4.1.6>`_,
319321 "the certificate containing
320322 the public key corresponding to the key used to digitally sign the
@@ -338,29 +340,26 @@ def __init__(
338340
339341 .. admonition:: Supporting reading client certificates from PFX files
340342
343+ This usage will automatically use SHA-256 thumbprint of the certificate.
344+
341345 *Added in version 1.29.0*:
342346 Feed in a dictionary containing the path to a PFX file::
343347
344348 {
345- "private_key_pfx_path": "/path/to/your.pfx",
349+ "private_key_pfx_path": "/path/to/your.pfx", # Added in version 1.29.0
350+ "public_certificate": True, # Only needed if you use Subject Name/Issuer auth. Added in version 1.30.0
346351 "passphrase": "Passphrase if the private_key is encrypted (Optional)",
347352 }
348353
349354 The following command will generate a .pfx file from your .key and .pem file::
350355
351356 openssl pkcs12 -export -out certificate.pfx -inkey privateKey.key -in certificate.pem
352357
353- .. admonition:: Support Subject Name/Issuer Auth with a cert in .pfx
354-
355- *Added in version 1.30.0*:
358+ ` Subject Name/Issuer Auth
359+ <https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/60>`_
360+ is an approach to allow easier certificate rotation.
356361 If your .pfx file contains both the private key and public cert,
357- you can opt in for Subject Name/Issuer Auth like this::
358-
359- {
360- "private_key_pfx_path": "/path/to/your.pfx",
361- "public_certificate": True,
362- "passphrase": "Passphrase if the private_key is encrypted (Optional)",
363- }
362+ you can opt in for Subject Name/Issuer Auth by setting "public_certificate" to ``True``.
364363
365364 :type client_credential: Union[dict, str, None]
366365
@@ -815,15 +814,30 @@ def _build_client(self, client_credential, authority, skip_regional_client=False
815814 passphrase_bytes )
816815 if client_credential .get ("public_certificate" ) is True and x5c :
817816 headers ["x5c" ] = x5c
818- elif (
819- client_credential .get ("private_key" ) # PEM blob
820- and client_credential .get ("thumbprint" )):
821- sha1_thumbprint = client_credential ["thumbprint" ]
822- if passphrase_bytes :
823- private_key = _load_private_key_from_pem_str (
817+ elif client_credential .get ("private_key" ): # PEM blob
818+ private_key = ( # handles both encrypted and unencrypted
819+ _load_private_key_from_pem_str (
824820 client_credential ['private_key' ], passphrase_bytes )
825- else : # PEM without passphrase
826- private_key = client_credential ['private_key' ]
821+ if passphrase_bytes
822+ else client_credential ['private_key' ]
823+ )
824+
825+ # 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+ elif isinstance (client_credential .get ('public_certificate' ), str ):
831+ # No thumbprint provided, but we have a certificate to calculate thumbprints
832+ from cryptography import x509
833+ cert = x509 .load_pem_x509_certificate (
834+ _str2bytes (client_credential ['public_certificate' ]))
835+ sha256_thumbprint , sha1_thumbprint , headers ["x5c" ] = (
836+ _extract_cert_and_thumbprints (cert ))
837+ else :
838+ raise ValueError (
839+ "You must provide either 'thumbprint' or 'public_certificate' "
840+ "from which the thumbprint can be calculated." )
827841 else :
828842 raise ValueError (
829843 "client_credential needs to follow this format "
@@ -947,7 +961,7 @@ def initiate_auth_code_flow(
947961
948962 :param str response_mode:
949963 OPTIONAL. Specifies the method with which response parameters should be returned.
950- The default value is equivalent to ``query``, which is still secure enough in MSAL Python
964+ The default value is equivalent to ``query``, which was still secure enough in MSAL Python
951965 (because MSAL Python does not transfer tokens via query parameter in the first place).
952966 For even better security, we recommend using the value ``form_post``.
953967 In "form_post" mode, response parameters
@@ -959,6 +973,11 @@ def initiate_auth_code_flow(
959973 `here <https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes>`
960974 and `here <https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html#FormPostResponseMode>`
961975
976+ .. note::
977+ You should configure your web framework to accept form_post responses instead of query responses.
978+ While this parameter still works, it will be removed in a future version.
979+ Using query-based response modes is less secure and should be avoided.
980+
962981 :return:
963982 The auth code flow. It is a dict in this form::
964983
@@ -977,6 +996,9 @@ def initiate_auth_code_flow(
977996 3. and then relay this dict and subsequent auth response to
978997 :func:`~acquire_token_by_auth_code_flow()`.
979998 """
999+ # Note to maintainers: Do not emit warning for the use of response_mode here,
1000+ # because response_mode=form_post is still the recommended usage for MSAL Python 1.x.
1001+ # App developers making the right call shall not be disturbed by unactionable warnings.
9801002 client = _ClientWithCcsRoutingInfo (
9811003 {"authorization_endpoint" : self .authority .authorization_endpoint },
9821004 self .client_id ,
@@ -1840,7 +1862,17 @@ def acquire_token_by_username_password(
18401862
18411863 - A successful response would contain "access_token" key,
18421864 - an error response would contain "error" and usually "error_description".
1865+
1866+ [Deprecated] This API is deprecated for public client flows and will be
1867+ removed in a future release. Use a more secure flow instead.
1868+ Migration guide: https://aka.ms/msal-ropc-migration
1869+
18431870 """
1871+ is_confidential_app = self .client_credential or isinstance (
1872+ self , ConfidentialClientApplication )
1873+ if not is_confidential_app :
1874+ warnings .warn ("""This API has been deprecated for public client flows, please use a more secure flow.
1875+ See https://aka.ms/msal-ropc-migration for migration guidance""" , DeprecationWarning )
18441876 claims = _merge_claims_challenge_and_capabilities (
18451877 self ._client_capabilities , claims_challenge )
18461878 if self ._enable_broker and sys .platform in ("win32" , "darwin" ):
0 commit comments