Skip to content

MSAL Python 1.23.0 #581

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jul 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fd0a277
Merge branch 'release-1.22.0' into dev
rayluo Apr 7, 2023
f788a3b
Turns out they changed to a new tag for MSAL. Fix #539
rayluo Mar 30, 2023
2afec13
Merge branch 'fix-stackoverflow-link' into dev
rayluo Mar 31, 2023
2168027
Clarify that allow_broker is not applicable to ConfidentialClientAppl…
rayluo Mar 18, 2023
e8ff807
Merge pull request #559 from AzureAD/docs-staging
rayluo May 7, 2023
79efb89
Backport test improvements
rayluo Feb 26, 2023
a07c3ef
Expand http interface to include response.headers
rayluo Mar 4, 2023
fd57dc4
Merge remote-tracking branch 'oauth2cli/dev' into http-interface-polish
rayluo Mar 14, 2023
2175502
No need for DummyHttpResponse
rayluo Mar 16, 2023
56256e4
Merge branch 'http-interface-polish' into dev
rayluo Mar 21, 2023
49090cb
Adjustment for new CIAM partition
rayluo Apr 29, 2023
b96489d
Merge pull request #564 from AzureAD/ciam-in-new-partition
rayluo May 24, 2023
6b2f337
Improve logs
rayluo May 26, 2023
7aa2078
Merge branch 'improve-logs' into dev
rayluo May 30, 2023
1b7db8d
Add more sections into TOC for the now long doc
rayluo May 31, 2023
10c8dd5
Remove many Sphinx warnings
rayluo Jun 2, 2023
613f389
Merge branch 'docs-staging' into dev
rayluo Jun 6, 2023
3e3b97a
Github removes Python 2.7 support on 2023-6-19
rayluo Jun 8, 2023
e1e3d1c
Merge branch 'fix-build-error' into dev
rayluo Jun 10, 2023
2288b77
Remove acquire_token_silent(..., account=None) usage in a backward-co…
rayluo Jun 29, 2023
1b316e3
Merge pull request #577 from AzureAD/silent-adjustment
rayluo Jul 22, 2023
44c3bfb
Bumping up version numbers
rayluo Jul 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest # It switched to 22.04 shortly after 2022-Nov-8
strategy:
matrix:
python-version: [2.7, 3.7, 3.8, 3.9, "3.10", "3.11", "3.12-dev"]
python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12-dev"]

steps:
- uses: actions/checkout@v2
Expand Down
31 changes: 20 additions & 11 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
MSAL Python documentation
MSAL Python Documentation
=========================

.. toctree::
:maxdepth: 2
:caption: Contents:
:hidden:

MSAL Documentation <https://docs.microsoft.com/en-au/azure/active-directory/develop/msal-authentication-flows>
GitHub Repository <https://github.com/AzureAD/microsoft-authentication-library-for-python>
index

..
Comment: Perhaps because of the theme, only the first level sections will show in TOC,
regardless of maxdepth setting.

You can find high level conceptual documentations in the project
`README <https://github.com/AzureAD/microsoft-authentication-library-for-python>`_.
Expand Down Expand Up @@ -58,8 +61,8 @@ MSAL Python supports some of them.
<https://github.com/AzureAD/microsoft-authentication-library-for-python/tree/dev/sample>`_.


API
===
API Reference
=============

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

ClientApplication
=================

.. autoclass:: msal.ClientApplication
:members:
:inherited-members:

.. automethod:: __init__

PublicClientApplication
-----------------------
=======================

.. autoclass:: msal.PublicClientApplication
:members:
:inherited-members:

.. automethod:: __init__

ConfidentialClientApplication
-----------------------------
=============================

.. autoclass:: msal.ConfidentialClientApplication
:members:
:inherited-members:

.. automethod:: __init__

TokenCache
----------
==========

One of the parameters accepted by
both `PublicClientApplication` and `ConfidentialClientApplication`
Expand Down
136 changes: 96 additions & 40 deletions msal/application.py

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions msal/oauth2cli/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ class Response(object):
# but a `text` would be more generic,
# when downstream packages would potentially access some XML endpoints.

headers = {} # Duplicated headers are expected to be combined into one header
# with its value as a comma-separated string.
# https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.2
# Popular HTTP libraries model it as a case-insensitive dict.

def raise_for_status(self):
"""Raise an exception when http response status contains error"""
raise NotImplementedError("Your implementation should provide this")
Expand Down
1 change: 0 additions & 1 deletion msal/token_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ def find(self, credential_type, target=None, query=None):
]

def add(self, event, now=None):
# type: (dict) -> None
"""Handle a token obtaining event, and add tokens into cache."""
def make_clean_copy(dictionary, sensitive_fields): # Masks sensitive info
return {
Expand Down
14 changes: 3 additions & 11 deletions sample/confidential_client_certificate_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,9 @@
# https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache
)

# The pattern to acquire a token looks like this.
result = None

# Firstly, looks up a token from cache
# Since we are looking for token for the current app, NOT for an end user,
# notice we give account parameter as None.
result = app.acquire_token_silent(config["scope"], account=None)

if not result:
logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
result = app.acquire_token_for_client(scopes=config["scope"])
# Since MSAL 1.23, acquire_token_for_client(...) will automatically look up
# a token from cache, and fall back to acquire a fresh token when needed.
result = app.acquire_token_for_client(scopes=config["scope"])

if "access_token" in result:
# Calling graph using the access token
Expand Down
14 changes: 3 additions & 11 deletions sample/confidential_client_secret_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,9 @@
# https://msal-python.readthedocs.io/en/latest/#msal.SerializableTokenCache
)

# The pattern to acquire a token looks like this.
result = None

# Firstly, looks up a token from cache
# Since we are looking for token for the current app, NOT for an end user,
# notice we give account parameter as None.
result = app.acquire_token_silent(config["scope"], account=None)

if not result:
logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
result = app.acquire_token_for_client(scopes=config["scope"])
# Since MSAL 1.23, acquire_token_for_client(...) will automatically look up
# a token from cache, and fall back to acquire a fresh token when needed.
result = app.acquire_token_for_client(scopes=config["scope"])

if "access_token" in result:
# Calling graph using the access token
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ universal=1
project_urls =
Changelog = https://github.com/AzureAD/microsoft-authentication-library-for-python/releases
Documentation = https://msal-python.readthedocs.io/
Questions = https://stackoverflow.com/questions/tagged/msal+python
Questions = https://stackoverflow.com/questions/tagged/azure-ad-msal+python
Feature/Bug Tracker = https://github.com/AzureAD/microsoft-authentication-library-for-python/issues
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
'requests>=2.0.0,<3',
'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+

'cryptography>=0.6,<43',
'cryptography>=0.6,<44',
# load_pem_private_key() is available since 0.6
# https://github.com/pyca/cryptography/blob/master/CHANGELOG.rst#06---2014-09-29
#
Expand Down
5 changes: 3 additions & 2 deletions tests/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ def close(self): # Not required, but we use it to avoid a warning in unit test


class MinimalResponse(object): # Not for production use
def __init__(self, requests_resp=None, status_code=None, text=None):
def __init__(self, requests_resp=None, status_code=None, text=None, headers=None):
self.status_code = status_code or requests_resp.status_code
self.text = text or requests_resp.text
self.text = text if text is not None else requests_resp.text
self.headers = {} if headers is None else headers
self._raw_resp = requests_resp

def raise_for_status(self):
Expand Down
31 changes: 25 additions & 6 deletions tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,8 +382,8 @@ def test_aging_token_and_unavailable_aad_should_return_old_token(self):
old_at = "old AT"
self.populate_cache(access_token=old_at, expires_in=3599, refresh_in=-1)
def mock_post(url, headers=None, *args, **kwargs):
self.assertEqual("4|84,2|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
return MinimalResponse(status_code=400, text=json.dumps({"error": error}))
self.assertEqual("4|84,4|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
return MinimalResponse(status_code=400, text=json.dumps({"error": "foo"}))
result = self.app.acquire_token_silent(['s1'], self.account, post=mock_post)
self.assertEqual(old_at, result.get("access_token"))

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

def test_acquire_token_for_client(self):
at = "this is an access token"
def mock_post(url, headers=None, *args, **kwargs):
self.assertEqual("4|730,0|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
return MinimalResponse(status_code=200, text=json.dumps({"access_token": at}))
self.assertEqual("4|730,2|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
return MinimalResponse(status_code=200, text=json.dumps({
"access_token": "AT 1",
"expires_in": 0,
}))
result = self.app.acquire_token_for_client(["scope"], post=mock_post)
self.assertEqual(at, result.get("access_token"))
self.assertEqual("AT 1", result.get("access_token"), "Shall get a new token")

def mock_post(url, headers=None, *args, **kwargs):
self.assertEqual("4|730,3|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
return MinimalResponse(status_code=200, text=json.dumps({
"access_token": "AT 2",
"expires_in": 3600,
"refresh_in": -100, # A hack to make sure it will attempt refresh
}))
result = self.app.acquire_token_for_client(["scope"], post=mock_post)
self.assertEqual("AT 2", result.get("access_token"), "Shall get a new token")

def mock_post(url, headers=None, *args, **kwargs):
# 1/0 # TODO: Make sure this was called
self.assertEqual("4|730,4|", (headers or {}).get(CLIENT_CURRENT_TELEMETRY))
return MinimalResponse(status_code=400, text=json.dumps({"error": "foo"}))
result = self.app.acquire_token_for_client(["scope"], post=mock_post)
self.assertEqual("AT 2", result.get("access_token"), "Shall get aging token")

def test_acquire_token_on_behalf_of(self):
at = "this is an access token"
Expand Down
20 changes: 12 additions & 8 deletions tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,17 +146,15 @@ def assertCacheWorksForApp(self, result_from_wire, scope):
json.dumps(self.app.token_cache._cache, indent=4),
json.dumps(result_from_wire.get("id_token_claims"), indent=4),
)
# Going to test acquire_token_silent(...) to locate an AT from cache
result_from_cache = self.app.acquire_token_silent(scope, account=None)
self.assertIsNone(
self.app.acquire_token_silent(scope, account=None),
"acquire_token_silent(..., account=None) shall always return None")
# Going to test acquire_token_for_client(...) to locate an AT from cache
result_from_cache = self.app.acquire_token_for_client(scope)
self.assertIsNotNone(result_from_cache)
self.assertEqual(
result_from_wire['access_token'], result_from_cache['access_token'],
"We should get a cached AT")
self.app.acquire_token_silent(
# Result will typically be None, because client credential grant returns no RT.
# But we care more on this call should succeed without exception.
scope, account=None,
force_refresh=True) # Mimic the AT already expires

@classmethod
def _build_app(cls,
Expand Down Expand Up @@ -925,10 +923,16 @@ def test_ciam_acquire_token_for_client(self):
client_secret=self.get_lab_user_secret(
self.app_config["clientSecret"].split("=")[-1]),
authority=self.app_config["authority"],
scope=["{}/.default".format(self.app_config["appId"])], # App permission
#scope=["{}/.default".format(self.app_config["appId"])], # AADSTS500207: The account type can't be used for the resource you're trying to access.
#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.
scope=self.app_config["scopes"], # It shall ends with "/.default"
)

def test_ciam_acquire_token_by_ropc(self):
"""CIAM does not officially support ROPC, especially not for external emails.

We keep this test case for now, because the test data will use a local email.
"""
# Somehow, this would only work after creating a secret for the test app
# and enabling "Allow public client flows".
# Otherwise it would hit AADSTS7000218.
Expand Down
8 changes: 1 addition & 7 deletions tests/test_throttled_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,13 @@
logging.basicConfig(level=logging.DEBUG)


class DummyHttpResponse(MinimalResponse):
def __init__(self, headers=None, **kwargs):
self.headers = {} if headers is None else headers
super(DummyHttpResponse, self).__init__(**kwargs)


class DummyHttpClient(object):
def __init__(self, status_code=None, response_headers=None):
self._status_code = status_code
self._response_headers = response_headers

def _build_dummy_response(self):
return DummyHttpResponse(
return MinimalResponse(
status_code=self._status_code,
headers=self._response_headers,
text=random(), # So that we'd know whether a new response is received
Expand Down