Skip to content

Commit 1b09a45

Browse files
authored
Merge pull request #272 from AzureAD/release-1.6.0
MSAL Python 1.6.0
2 parents 410635e + da5baf3 commit 1b09a45

File tree

4 files changed

+55
-8
lines changed

4 files changed

+55
-8
lines changed

msal/application.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222

2323
# The __init__.py will import this. Not the other way around.
24-
__version__ = "1.5.1"
24+
__version__ = "1.6.0"
2525

2626
logger = logging.getLogger(__name__)
2727

@@ -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"
@@ -123,7 +131,8 @@ def __init__(
123131
{
124132
"private_key": "...-----BEGIN PRIVATE KEY-----...",
125133
"thumbprint": "A1B2C3D4E5F6...",
126-
"public_certificate": "...-----BEGIN CERTIFICATE-----..." (Optional. See below.)
134+
"public_certificate": "...-----BEGIN CERTIFICATE-----... (Optional. See below.)",
135+
"passphrase": "Passphrase if the private_key is encrypted (Optional. Added in version 1.6.0)",
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,

msal/authority.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def __init__(self, authority_url, http_client, validate_authority=True):
8383
openid_config = tenant_discovery(
8484
tenant_discovery_endpoint,
8585
self.http_client)
86-
except ValueError: # json.decoder.JSONDecodeError in Py3 subclasses this
86+
except ValueError:
8787
raise ValueError(
8888
"Unable to get authority configuration for {}. "
8989
"Authority would typically be in a format of "
@@ -140,8 +140,17 @@ def instance_discovery(url, http_client, **kwargs):
140140
def tenant_discovery(tenant_discovery_endpoint, http_client, **kwargs):
141141
# Returns Openid Configuration
142142
resp = http_client.get(tenant_discovery_endpoint, **kwargs)
143-
payload = json.loads(resp.text)
144-
if 'authorization_endpoint' in payload and 'token_endpoint' in payload:
145-
return payload
146-
raise MsalServiceError(status_code=resp.status_code, **payload)
143+
if resp.status_code == 200:
144+
payload = json.loads(resp.text) # It could raise ValueError
145+
if 'authorization_endpoint' in payload and 'token_endpoint' in payload:
146+
return payload # Happy path
147+
raise ValueError("OIDC Discovery does not provide enough information")
148+
if 400 <= resp.status_code < 500:
149+
# Nonexist tenant would hit this path
150+
# e.g. https://login.microsoftonline.com/nonexist_tenant/v2.0/.well-known/openid-configuration
151+
raise ValueError("OIDC Discovery endpoint rejects our request")
152+
# Transient network error would hit this path
153+
resp.raise_for_status()
154+
raise RuntimeError( # A fallback here, in case resp.raise_for_status() is no-op
155+
"Unable to complete OIDC Discovery: %d, %s" % (resp.status_code, resp.text))
147156

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)