Skip to content

Commit 1318025

Browse files
authored
Merge pull request #581 from AzureAD/release-1.23.0
MSAL Python 1.23.0
2 parents dabc08c + 44c3bfb commit 1318025

13 files changed

+171
-100
lines changed

.github/workflows/python-package.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
runs-on: ubuntu-latest # It switched to 22.04 shortly after 2022-Nov-8
2727
strategy:
2828
matrix:
29-
python-version: [2.7, 3.7, 3.8, 3.9, "3.10", "3.11", "3.12-dev"]
29+
python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12-dev"]
3030

3131
steps:
3232
- uses: actions/checkout@v2

docs/index.rst

+20-11
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
MSAL Python documentation
1+
MSAL Python Documentation
22
=========================
33

44
.. toctree::
55
:maxdepth: 2
66
:caption: Contents:
77
:hidden:
88

9-
MSAL Documentation <https://docs.microsoft.com/en-au/azure/active-directory/develop/msal-authentication-flows>
10-
GitHub Repository <https://github.com/AzureAD/microsoft-authentication-library-for-python>
9+
index
10+
11+
..
12+
Comment: Perhaps because of the theme, only the first level sections will show in TOC,
13+
regardless of maxdepth setting.
1114
1215
You can find high level conceptual documentations in the project
1316
`README <https://github.com/AzureAD/microsoft-authentication-library-for-python>`_.
@@ -58,8 +61,8 @@ MSAL Python supports some of them.
5861
<https://github.com/AzureAD/microsoft-authentication-library-for-python/tree/dev/sample>`_.
5962

6063

61-
API
62-
===
64+
API Reference
65+
=============
6366

6467
The following section is the API Reference of MSAL Python.
6568
The API Reference is like a dictionary. You **read this API section when and only when**:
@@ -88,26 +91,32 @@ MSAL proposes a clean separation between
8891
They are implemented as two separated classes,
8992
with different methods for different authentication scenarios.
9093

94+
ClientApplication
95+
=================
96+
97+
.. autoclass:: msal.ClientApplication
98+
:members:
99+
:inherited-members:
100+
101+
.. automethod:: __init__
102+
91103
PublicClientApplication
92-
-----------------------
104+
=======================
93105

94106
.. autoclass:: msal.PublicClientApplication
95107
:members:
96-
:inherited-members:
97108

98109
.. automethod:: __init__
99110

100111
ConfidentialClientApplication
101-
-----------------------------
112+
=============================
102113

103114
.. autoclass:: msal.ConfidentialClientApplication
104115
:members:
105-
:inherited-members:
106116

107-
.. automethod:: __init__
108117

109118
TokenCache
110-
----------
119+
==========
111120

112121
One of the parameters accepted by
113122
both `PublicClientApplication` and `ConfidentialClientApplication`

msal/application.py

+96-40
Large diffs are not rendered by default.

msal/oauth2cli/http.py

+5
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ class Response(object):
5858
# but a `text` would be more generic,
5959
# when downstream packages would potentially access some XML endpoints.
6060

61+
headers = {} # Duplicated headers are expected to be combined into one header
62+
# with its value as a comma-separated string.
63+
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.2
64+
# Popular HTTP libraries model it as a case-insensitive dict.
65+
6166
def raise_for_status(self):
6267
"""Raise an exception when http response status contains error"""
6368
raise NotImplementedError("Your implementation should provide this")

msal/token_cache.py

-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,6 @@ def find(self, credential_type, target=None, query=None):
102102
]
103103

104104
def add(self, event, now=None):
105-
# type: (dict) -> None
106105
"""Handle a token obtaining event, and add tokens into cache."""
107106
def make_clean_copy(dictionary, sensitive_fields): # Masks sensitive info
108107
return {

sample/confidential_client_certificate_sample.py

+3-11
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,9 @@
5151
# https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache
5252
)
5353

54-
# The pattern to acquire a token looks like this.
55-
result = None
56-
57-
# Firstly, looks up a token from cache
58-
# Since we are looking for token for the current app, NOT for an end user,
59-
# notice we give account parameter as None.
60-
result = app.acquire_token_silent(config["scope"], account=None)
61-
62-
if not result:
63-
logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
64-
result = app.acquire_token_for_client(scopes=config["scope"])
54+
# Since MSAL 1.23, acquire_token_for_client(...) will automatically look up
55+
# a token from cache, and fall back to acquire a fresh token when needed.
56+
result = app.acquire_token_for_client(scopes=config["scope"])
6557

6658
if "access_token" in result:
6759
# Calling graph using the access token

sample/confidential_client_secret_sample.py

+3-11
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,9 @@
5050
# https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache
5151
)
5252

53-
# The pattern to acquire a token looks like this.
54-
result = None
55-
56-
# Firstly, looks up a token from cache
57-
# Since we are looking for token for the current app, NOT for an end user,
58-
# notice we give account parameter as None.
59-
result = app.acquire_token_silent(config["scope"], account=None)
60-
61-
if not result:
62-
logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
63-
result = app.acquire_token_for_client(scopes=config["scope"])
53+
# Since MSAL 1.23, acquire_token_for_client(...) will automatically look up
54+
# a token from cache, and fall back to acquire a fresh token when needed.
55+
result = app.acquire_token_for_client(scopes=config["scope"])
6456

6557
if "access_token" in result:
6658
# Calling graph using the access token

setup.cfg

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ universal=1
55
project_urls =
66
Changelog = https://github.com/AzureAD/microsoft-authentication-library-for-python/releases
77
Documentation = https://msal-python.readthedocs.io/
8-
Questions = https://stackoverflow.com/questions/tagged/msal+python
8+
Questions = https://stackoverflow.com/questions/tagged/azure-ad-msal+python
99
Feature/Bug Tracker = https://github.com/AzureAD/microsoft-authentication-library-for-python/issues

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
'requests>=2.0.0,<3',
7878
'PyJWT[crypto]>=1.0.0,<3', # MSAL does not use jwt.decode(), therefore is insusceptible to CVE-2022-29217 so no need to bump to PyJWT 2.4+
7979

80-
'cryptography>=0.6,<43',
80+
'cryptography>=0.6,<44',
8181
# load_pem_private_key() is available since 0.6
8282
# https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst#06---2014-09-29
8383
#

tests/http_client.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ def close(self): # Not required, but we use it to avoid a warning in unit test
2525

2626

2727
class MinimalResponse(object): # Not for production use
28-
def __init__(self, requests_resp=None, status_code=None, text=None):
28+
def __init__(self, requests_resp=None, status_code=None, text=None, headers=None):
2929
self.status_code = status_code or requests_resp.status_code
30-
self.text = text or requests_resp.text
30+
self.text = text if text is not None else requests_resp.text
31+
self.headers = {} if headers is None else headers
3132
self._raw_resp = requests_resp
3233

3334
def raise_for_status(self):

tests/test_application.py

+25-6
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,8 @@ def test_aging_token_and_unavailable_aad_should_return_old_token(self):
382382
old_at = "old AT"
383383
self.populate_cache(access_token=old_at, expires_in=3599, refresh_in=-1)
384384
def mock_post(url, headers=None, *args, **kwargs):
385-
self.assertEqual("4|84,2|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
386-
return MinimalResponse(status_code=400, text=json.dumps({"error": error}))
385+
self.assertEqual("4|84,4|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
386+
return MinimalResponse(status_code=400, text=json.dumps({"error": "foo"}))
387387
result = self.app.acquire_token_silent(['s1'], self.account, post=mock_post)
388388
self.assertEqual(old_at, result.get("access_token"))
389389

@@ -549,12 +549,31 @@ def setUpClass(cls): # Initialization at runtime, not interpret-time
549549
authority="https://login.microsoftonline.com/common")
550550

551551
def test_acquire_token_for_client(self):
552-
at = "this is an access token"
553552
def mock_post(url, headers=None, *args, **kwargs):
554-
self.assertEqual("4|730,0|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
555-
return MinimalResponse(status_code=200, text=json.dumps({"access_token": at}))
553+
self.assertEqual("4|730,2|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
554+
return MinimalResponse(status_code=200, text=json.dumps({
555+
"access_token": "AT 1",
556+
"expires_in": 0,
557+
}))
556558
result = self.app.acquire_token_for_client(["scope"], post=mock_post)
557-
self.assertEqual(at, result.get("access_token"))
559+
self.assertEqual("AT 1", result.get("access_token"), "Shall get a new token")
560+
561+
def mock_post(url, headers=None, *args, **kwargs):
562+
self.assertEqual("4|730,3|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
563+
return MinimalResponse(status_code=200, text=json.dumps({
564+
"access_token": "AT 2",
565+
"expires_in": 3600,
566+
"refresh_in": -100, # A hack to make sure it will attempt refresh
567+
}))
568+
result = self.app.acquire_token_for_client(["scope"], post=mock_post)
569+
self.assertEqual("AT 2", result.get("access_token"), "Shall get a new token")
570+
571+
def mock_post(url, headers=None, *args, **kwargs):
572+
# 1/0 # TODO: Make sure this was called
573+
self.assertEqual("4|730,4|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
574+
return MinimalResponse(status_code=400, text=json.dumps({"error": "foo"}))
575+
result = self.app.acquire_token_for_client(["scope"], post=mock_post)
576+
self.assertEqual("AT 2", result.get("access_token"), "Shall get aging token")
558577

559578
def test_acquire_token_on_behalf_of(self):
560579
at = "this is an access token"

tests/test_e2e.py

+12-8
Original file line numberDiff line numberDiff line change
@@ -146,17 +146,15 @@ def assertCacheWorksForApp(self, result_from_wire, scope):
146146
json.dumps(self.app.token_cache._cache, indent=4),
147147
json.dumps(result_from_wire.get("id_token_claims"), indent=4),
148148
)
149-
# Going to test acquire_token_silent(...) to locate an AT from cache
150-
result_from_cache = self.app.acquire_token_silent(scope, account=None)
149+
self.assertIsNone(
150+
self.app.acquire_token_silent(scope, account=None),
151+
"acquire_token_silent(..., account=None) shall always return None")
152+
# Going to test acquire_token_for_client(...) to locate an AT from cache
153+
result_from_cache = self.app.acquire_token_for_client(scope)
151154
self.assertIsNotNone(result_from_cache)
152155
self.assertEqual(
153156
result_from_wire['access_token'], result_from_cache['access_token'],
154157
"We should get a cached AT")
155-
self.app.acquire_token_silent(
156-
# Result will typically be None, because client credential grant returns no RT.
157-
# But we care more on this call should succeed without exception.
158-
scope, account=None,
159-
force_refresh=True) # Mimic the AT already expires
160158

161159
@classmethod
162160
def _build_app(cls,
@@ -925,10 +923,16 @@ def test_ciam_acquire_token_for_client(self):
925923
client_secret=self.get_lab_user_secret(
926924
self.app_config["clientSecret"].split("=")[-1]),
927925
authority=self.app_config["authority"],
928-
scope=["{}/.default".format(self.app_config["appId"])], # App permission
926+
#scope=["{}/.default".format(self.app_config["appId"])], # AADSTS500207: The account type can't be used for the resource you're trying to access.
927+
#scope=["api://{}/.default".format(self.app_config["appId"])], # AADSTS500011: The resource principal named api://ced781e7-bdb0-4c99-855c-d3bacddea88a was not found in the tenant named MSIDLABCIAM2. This can happen if the application has not been installed by the administrator of the tenant or consented to by any user in the tenant. You might have sent your authentication request to the wrong tenant.
928+
scope=self.app_config["scopes"], # It shall ends with "/.default"
929929
)
930930

931931
def test_ciam_acquire_token_by_ropc(self):
932+
"""CIAM does not officially support ROPC, especially not for external emails.
933+
934+
We keep this test case for now, because the test data will use a local email.
935+
"""
932936
# Somehow, this would only work after creating a secret for the test app
933937
# and enabling "Allow public client flows".
934938
# Otherwise it would hit AADSTS7000218.

tests/test_throttled_http_client.py

+1-7
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,13 @@
1111
logging.basicConfig(level=logging.DEBUG)
1212

1313

14-
class DummyHttpResponse(MinimalResponse):
15-
def __init__(self, headers=None, **kwargs):
16-
self.headers = {} if headers is None else headers
17-
super(DummyHttpResponse, self).__init__(**kwargs)
18-
19-
2014
class DummyHttpClient(object):
2115
def __init__(self, status_code=None, response_headers=None):
2216
self._status_code = status_code
2317
self._response_headers = response_headers
2418

2519
def _build_dummy_response(self):
26-
return DummyHttpResponse(
20+
return MinimalResponse(
2721
status_code=self._status_code,
2822
headers=self._response_headers,
2923
text=random(), # So that we'd know whether a new response is received

0 commit comments

Comments
 (0)