Skip to content

Commit c37f36b

Browse files
authored
Merge pull request #270 from AzureAD/private-key-with-password
Passing encrypted key as credential
2 parents 1f56396 + 2ded277 commit c37f36b

File tree

3 files changed

+39
-1
lines changed

3 files changed

+39
-1
lines changed

msal/application.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ def _merge_claims_challenge_and_capabilities(capabilities, claims_challenge):
9090
return json.dumps(claims_dict)
9191

9292

93+
def _str2bytes(raw):
94+
# A conversion based on duck-typing rather than six.text_type
95+
try:
96+
return raw.encode(encoding="utf-8")
97+
except:
98+
return raw
99+
100+
93101
class ClientApplication(object):
94102

95103
ACQUIRE_TOKEN_SILENT_ID = "84"
@@ -124,6 +132,7 @@ def __init__(
124132
"private_key": "...-----BEGIN PRIVATE KEY-----...",
125133
"thumbprint": "A1B2C3D4E5F6...",
126134
"public_certificate": "...-----BEGIN CERTIFICATE-----..." (Optional. See below.)
135+
"passphrase": "Passphrase if the private_key is encrypted (Optional)"
127136
}
128137
129138
*Added in version 0.5.0*:
@@ -252,8 +261,18 @@ def _build_client(self, client_credential, authority):
252261
headers = {}
253262
if 'public_certificate' in client_credential:
254263
headers["x5c"] = extract_certs(client_credential['public_certificate'])
264+
if not client_credential.get("passphrase"):
265+
unencrypted_private_key = client_credential['private_key']
266+
else:
267+
from cryptography.hazmat.primitives import serialization
268+
from cryptography.hazmat.backends import default_backend
269+
unencrypted_private_key = serialization.load_pem_private_key(
270+
_str2bytes(client_credential["private_key"]),
271+
_str2bytes(client_credential["passphrase"]),
272+
backend=default_backend(), # It was a required param until 2020
273+
)
255274
assertion = JwtAssertionCreator(
256-
client_credential["private_key"], algorithm="RS256",
275+
unencrypted_private_key, algorithm="RS256",
257276
sha1_thumbprint=client_credential.get("thumbprint"), headers=headers)
258277
client_assertion = assertion.create_regenerative_assertion(
259278
audience=authority.token_endpoint, issuer=self.client_id,

setup.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@
7474
install_requires=[
7575
'requests>=2.0.0,<3',
7676
'PyJWT[crypto]>=1.0.0,<2',
77+
78+
'cryptography>=0.6,<4',
79+
# load_pem_private_key() is available since 0.6
80+
# https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst#06---2014-09-29
81+
#
82+
# Not sure what should be used as an upper bound here
83+
# https://github.com/pyca/cryptography/issues/5532
84+
# We will go with "<4" for now, which is also what our another dependency,
85+
# pyjwt, currently use.
86+
7787
]
7888
)
7989

tests/test_application.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Note: Since Aug 2019 we move all e2e tests into test_e2e.py,
22
# so this test_application file contains only unit tests without dependency.
33
from msal.application import *
4+
from msal.application import _str2bytes
45
import msal
56
from msal.application import _merge_claims_challenge_and_capabilities
67
from tests import unittest
@@ -39,6 +40,14 @@ def test_extract_multiple_tag_enclosed_certs(self):
3940
self.assertEqual(["my_cert1", "my_cert2"], extract_certs(pem))
4041

4142

43+
class TestBytesConversion(unittest.TestCase):
44+
def test_string_to_bytes(self):
45+
self.assertEqual(type(_str2bytes("some string")), type(b"bytes"))
46+
47+
def test_bytes_to_bytes(self):
48+
self.assertEqual(type(_str2bytes(b"some bytes")), type(b"bytes"))
49+
50+
4251
class TestClientApplicationAcquireTokenSilentErrorBehaviors(unittest.TestCase):
4352

4453
def setUp(self):

0 commit comments

Comments
 (0)