@@ -65,6 +65,29 @@ def _str2bytes(raw):
6565 return raw
6666
6767
68+ def _load_private_key_from_pfx_path (pfx_path , passphrase_bytes ):
69+ # Cert concepts https://security.stackexchange.com/a/226758/125264
70+ from cryptography .hazmat .primitives import hashes
71+ from cryptography .hazmat .primitives .serialization import pkcs12
72+ with open (pfx_path , 'rb' ) as f :
73+ private_key , cert , _ = pkcs12 .load_key_and_certificates ( # cryptography 2.5+
74+ # https://cryptography.io/en/latest/hazmat/primitives/asymmetric/serialization/#cryptography.hazmat.primitives.serialization.pkcs12.load_key_and_certificates
75+ f .read (), passphrase_bytes )
76+ sha1_thumbprint = cert .fingerprint (hashes .SHA1 ()).hex () # cryptography 0.7+
77+ # https://cryptography.io/en/latest/x509/reference/#x-509-certificate-object
78+ return private_key , sha1_thumbprint
79+
80+
81+ def _load_private_key_from_pem_str (private_key_pem_str , passphrase_bytes ):
82+ from cryptography .hazmat .primitives import serialization
83+ from cryptography .hazmat .backends import default_backend
84+ return serialization .load_pem_private_key ( # cryptography 0.6+
85+ _str2bytes (private_key_pem_str ),
86+ passphrase_bytes ,
87+ backend = default_backend (), # It was a required param until 2020
88+ )
89+
90+
6891def _pii_less_home_account_id (home_account_id ):
6992 parts = home_account_id .split ("." ) # It could contain one or two parts
7093 parts [0 ] = "********"
@@ -254,6 +277,16 @@ def __init__(
254277 "client_assertion": "...a JWT with claims aud, exp, iss, jti, nbf, and sub..."
255278 }
256279
280+ .. admonition:: Supporting reading client cerficates from PFX files
281+
282+ *Added in version 1.29.0*:
283+ Feed in a dictionary containing the path to a PFX file::
284+
285+ {
286+ "private_key_pfx_path": "/path/to/your.pfx",
287+ "passphrase": "Passphrase if the private_key is encrypted (Optional. Added in version 1.6.0)",
288+ }
289+
257290 :type client_credential: Union[dict, str]
258291
259292 :param dict client_claims:
@@ -651,29 +684,37 @@ def _build_client(self, client_credential, authority, skip_regional_client=False
651684 default_headers ['x-app-ver' ] = self .app_version
652685 default_body = {"client_info" : 1 }
653686 if isinstance (client_credential , dict ):
654- assert (("private_key" in client_credential
655- and "thumbprint" in client_credential ) or
656- "client_assertion" in client_credential )
657687 client_assertion_type = Client .CLIENT_ASSERTION_TYPE_JWT
658- if 'client_assertion' in client_credential :
688+ # Use client_credential.get("...") rather than "..." in client_credential
689+ # so that we can ignore an empty string came from an empty ENV VAR.
690+ if client_credential .get ("client_assertion" ):
659691 client_assertion = client_credential ['client_assertion' ]
660692 else :
661693 headers = {}
662- if 'public_certificate' in client_credential :
694+ if client_credential . get ( 'public_certificate' ) :
663695 headers ["x5c" ] = extract_certs (client_credential ['public_certificate' ])
664- if not client_credential .get ("passphrase" ):
665- unencrypted_private_key = client_credential ['private_key' ]
696+ passphrase_bytes = _str2bytes (
697+ client_credential ["passphrase" ]
698+ ) if client_credential .get ("passphrase" ) else None
699+ if client_credential .get ("private_key_pfx_path" ):
700+ private_key , sha1_thumbprint = _load_private_key_from_pfx_path (
701+ client_credential ["private_key_pfx_path" ], passphrase_bytes )
702+ elif (
703+ client_credential .get ("private_key" ) # PEM blob
704+ and client_credential .get ("thumbprint" )):
705+ sha1_thumbprint = client_credential ["thumbprint" ]
706+ if passphrase_bytes :
707+ private_key = _load_private_key_from_pem_str (
708+ client_credential ['private_key' ], passphrase_bytes )
709+ else : # PEM without passphrase
710+ private_key = client_credential ['private_key' ]
666711 else :
667- from cryptography .hazmat .primitives import serialization
668- from cryptography .hazmat .backends import default_backend
669- unencrypted_private_key = serialization .load_pem_private_key (
670- _str2bytes (client_credential ["private_key" ]),
671- _str2bytes (client_credential ["passphrase" ]),
672- backend = default_backend (), # It was a required param until 2020
673- )
712+ raise ValueError (
713+ "client_credential needs to follow this format "
714+ "https://msal-python.readthedocs.io/en/latest/#msal.ClientApplication.params.client_credential" )
674715 assertion = JwtAssertionCreator (
675- unencrypted_private_key , algorithm = "RS256" ,
676- sha1_thumbprint = client_credential . get ( "thumbprint" ) , headers = headers )
716+ private_key , algorithm = "RS256" ,
717+ sha1_thumbprint = sha1_thumbprint , headers = headers )
677718 client_assertion = assertion .create_regenerative_assertion (
678719 audience = authority .token_endpoint , issuer = self .client_id ,
679720 additional_claims = self .client_claims or {})
0 commit comments