Skip to content

Commit 192a82d

Browse files
authored
Removed support for Python 3.8 (#910)
* Added a soft warning if callback is not callable * updated crypto version max to 51 * removed support for 3.8 and added doc * Updated in warning placement * updated a fix for get()
1 parent 08aa7fd commit 192a82d

7 files changed

Lines changed: 257 additions & 8 deletions

File tree

.github/workflows/python-package.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
runs-on: ubuntu-22.04
2727
strategy:
2828
matrix:
29-
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
29+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
3030

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

contributing.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
Azure Active Directory SDK projects welcomes new contributors. This document will guide you
44
through the process.
55

6+
### SUPPORTED PYTHON VERSIONS
7+
8+
The set of Python versions that MSAL Python supports, and the policy for
9+
adding/removing support, is documented in
10+
[doc/python_version_support_policy.md](doc/python_version_support_policy.md).
11+
Any change that adds or removes a Python version must update both that
12+
policy document and the supported-version declarations it lists
13+
(`setup.cfg`, the GitHub Actions matrix, and the cryptography test).
14+
615
### CONTRIBUTOR LICENSE AGREEMENT
716

817
Please visit [https://cla.microsoft.com/](https://cla.microsoft.com/) and sign the Contributor License
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# MSAL Python Version Support Policy
2+
3+
This page describes the Python version support policy for the
4+
Microsoft Authentication Library for Python (MSAL Python), including
5+
end-of-support timelines for each Python version.
6+
7+
This policy is aligned with the
8+
[Azure SDK for Python version support policy](https://github.com/Azure/azure-sdk-for-python/blob/main/doc/python_version_support_policy.md)
9+
so that MSAL Python and the Azure SDK can be consumed together without
10+
version conflicts.
11+
12+
End of support means, in the MSAL Python context, that **new MSAL Python
13+
releases will no longer install on, be tested against, or accept bug
14+
fixes for those Python versions**. Older MSAL Python releases that did
15+
support those Python versions remain installable from PyPI via pip's
16+
`requires-python` resolution, so existing applications continue to work
17+
without change — they simply stop receiving new features and security
18+
fixes.
19+
20+
## Policy
21+
22+
MSAL Python supports a Python version while it is supported upstream by
23+
the Python core team (PSF), plus an additional **6-month grace window**
24+
after the PSF end-of-support date to give applications time to migrate.
25+
26+
Concretely:
27+
28+
- MSAL Python adds support for a new Python release as soon as practical
29+
after that Python release ships a stable `.0`.
30+
- MSAL Python drops support for a Python version on the **first MSAL
31+
Python release published on or after the SDK end-of-support date** for
32+
that Python version (PSF end-of-support + ~6 months).
33+
- Dropping a Python version is a **breaking change** and is delivered in
34+
a new minor or major release of MSAL Python, never in a patch.
35+
- The release notes (`RELEASES.md`) call out every Python-version
36+
removal, and `setup.cfg` is updated in the same change to bump
37+
`python_requires`, the trove classifiers, and any environment markers.
38+
39+
> **Note:** The "MSAL Python End Of Support" date is inclusive — the
40+
> listed day is the last supported day, and the next day is the first
41+
> unsupported day.
42+
43+
## Currently supported versions
44+
45+
| Python Version | PSF End of Support | MSAL Python End Of Support |
46+
|----------------|--------------------|----------------------------|
47+
| 3.9 ([PEP 596](https://peps.python.org/pep-0596/#lifespan)) | October 2025 | April 30, 2026 *(see note)* |
48+
| 3.10 ([PEP 619](https://peps.python.org/pep-0619/#lifespan)) | October 2026 | April 30, 2027 |
49+
| 3.11 ([PEP 664](https://peps.python.org/pep-0664/#lifespan)) | October 2027 | April 30, 2028 |
50+
| 3.12 ([PEP 693](https://peps.python.org/pep-0693/#lifespan)) | October 2028 | April 30, 2029 |
51+
| 3.13 ([PEP 719](https://peps.python.org/pep-0719/#lifespan)) | October 2029 | April 30, 2030 |
52+
| 3.14 ([PEP 745](https://peps.python.org/pep-0745/#lifespan)) | October 2030 | April 30, 2031 |
53+
54+
> **Note on Python 3.9:** Python 3.9 is past its policy end-of-support
55+
> date but is granted a one-time transition grace window in MSAL Python
56+
> while we adopt this policy and complete the removal of Python 3.8. It
57+
> will be removed in a subsequent MSAL Python release; the date will be
58+
> announced in `RELEASES.md` ahead of removal.
59+
60+
## End-of-life versions (no longer supported)
61+
62+
| Python Version | PSF End of Support | MSAL Python End Of Support |
63+
|----------------|--------------------|----------------------------|
64+
| 3.8 ([PEP 569](https://peps.python.org/pep-0569/#lifespan)) | October 2024 | April 2026 |
65+
| 3.7 ([PEP 537](https://peps.python.org/pep-0537/#lifespan)) | June 2023 | December 2023 |
66+
| 3.6 ([PEP 494](https://peps.python.org/pep-0494/#lifespan)) | December 2021 | August 2022 |
67+
| 2.7 ([PEP 373](https://peps.python.org/pep-0373/)) | April 2020 | January 2022 |
68+
69+
## Implementation
70+
71+
The supported Python versions are encoded in three places, which must be
72+
kept in sync with this policy:
73+
74+
1. **`setup.cfg`**`python_requires`, the `Programming Language ::
75+
Python :: 3.x` trove classifiers, and any `python_version`
76+
environment markers on optional dependencies (e.g. `pymsalruntime`).
77+
2. **`.github/workflows/python-package.yml`** — the `python-version`
78+
matrix used by the `pytest` test job.
79+
3. **`tests/test_cryptography.py`** — the N+3 ceiling test that enforces
80+
tracking the latest `cryptography` release. Newer `cryptography`
81+
versions routinely drop EOL Python versions, which is the most common
82+
forcing function for this policy.
83+
84+
## Rationale
85+
86+
MSAL Python depends transitively on `cryptography`, `requests`, and
87+
`PyJWT`. These libraries follow a similar policy and drop EOL Python
88+
versions roughly six months after PSF end-of-support. Continuing to
89+
support an EOL Python version in MSAL Python forces us to either pin
90+
those dependencies to old, unmaintained versions — exposing MSAL users
91+
to known CVEs — or to maintain conditional install metadata that breaks
92+
on every dependency bump. Aligning with the Azure SDK and upstream
93+
policies keeps MSAL Python simple, secure, and predictable.

msal/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
ConfidentialClientApplication,
3131
PublicClientApplication,
3232
)
33+
from .oauth2cli.assertion import AutoRefresher
3334
from .oauth2cli.oidc import Prompt, IdTokenError
3435
from .sku import __version__
3536
from .token_cache import TokenCache, SerializableTokenCache

msal/application.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,45 @@ def __init__(
344344
"client_assertion": "...a JWT with claims aud, exp, iss, jti, nbf, and sub..."
345345
}
346346
347+
.. note::
348+
349+
A pre-signed JWT string has a fixed expiration. Long-running
350+
confidential client applications (for example, workloads using
351+
AKS workload identity federation, or any other dynamic
352+
credential source) should instead pass a **callable** which
353+
MSAL will invoke on demand to obtain a fresh assertion::
354+
355+
def get_client_assertion():
356+
# e.g. read the projected service-account token from disk
357+
with open("/var/run/secrets/azure/tokens/azure-identity-token") as f:
358+
return f.read()
359+
360+
app = ConfidentialClientApplication(
361+
"client_id",
362+
client_credential={"client_assertion": get_client_assertion},
363+
...,
364+
)
365+
366+
The callable is only invoked when MSAL needs to send a token
367+
request on the wire (the in-memory token cache transparently
368+
avoids unnecessary calls).
369+
370+
If your callback is itself expensive (for example it calls
371+
out to a key vault), wrap it in :class:`msal.AutoRefresher`
372+
to memoize the assertion for its lifetime::
373+
374+
from msal import AutoRefresher
375+
smart_callback = AutoRefresher(get_client_assertion, expires_in=3600)
376+
app = ConfidentialClientApplication(
377+
"client_id",
378+
client_credential={"client_assertion": smart_callback},
379+
...,
380+
)
381+
382+
Passing a plain ``str`` / ``bytes`` ``client_assertion`` is
383+
still supported for backward compatibility but is discouraged
384+
because the assertion will eventually expire.
385+
347386
.. admonition:: Supporting reading client certificates from PFX files
348387
349388
This usage will automatically use SHA-256 thumbprint of the certificate.
@@ -677,6 +716,18 @@ def __init__(
677716
self._region_detected = None
678717
self.client, self._regional_client = self._build_client(
679718
client_credential, self.authority)
719+
# Warn if using a static string/bytes client_assertion (discouraged for long-running apps)
720+
if isinstance(client_credential, dict) and isinstance(
721+
client_credential.get("client_assertion"), (str, bytes)):
722+
warnings.warn(
723+
"Passing a static string/bytes 'client_assertion' is "
724+
"discouraged because the JWT will eventually expire. "
725+
"Pass a no-arg callable instead (optionally wrapped in "
726+
"msal.AutoRefresher) so MSAL can obtain a fresh "
727+
"assertion on demand. "
728+
"See https://github.com/AzureAD/microsoft-authentication-library-for-python/issues/746",
729+
DeprecationWarning, stacklevel=2)
730+
680731
self.authority_groups = {}
681732
self._telemetry_buffer = {}
682733
self._telemetry_lock = Lock()

setup.cfg

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ classifiers =
1818
Programming Language :: Python
1919
Programming Language :: Python :: 3 :: Only
2020
Programming Language :: Python :: 3
21-
Programming Language :: Python :: 3.8
2221
Programming Language :: Python :: 3.9
2322
Programming Language :: Python :: 3.10
2423
Programming Language :: Python :: 3.11
@@ -38,8 +37,9 @@ project_urls =
3837
[options]
3938
include_package_data = False # We used to ship LICENSE, but our __init__.py already mentions MIT
4039
packages = find:
41-
# Our test pipeline currently still covers Py37
42-
python_requires = >=3.8
40+
# Drop Python 3.8 because cryptography 48+ (and other key deps) no longer
41+
# support it; align with the cryptography upper bound policy.
42+
python_requires = >=3.9
4343
install_requires =
4444
requests>=2.0.0,<3
4545

@@ -53,7 +53,7 @@ install_requires =
5353
# And we will use the cryptography (X+3).0.0 as the upper bound,
5454
# based on their latest deprecation policy
5555
# https://cryptography.io/en/latest/api-stability/#deprecation
56-
cryptography>=2.5,<50
56+
cryptography>=2.5,<51
5757

5858

5959
[options.extras_require]
@@ -63,11 +63,12 @@ broker =
6363
# most existing MSAL Python apps do not have the redirect_uri needed by broker.
6464
#
6565
# We need pymsalruntime.CallbackData introduced in PyMsalRuntime 0.14
66-
pymsalruntime>=0.20.6,<0.21; python_version>='3.8' and platform_system=='Windows'
66+
# Using >=0.20 to accept all 0.20.x releases that include required features
67+
pymsalruntime>=0.20,<0.21; python_version>='3.9' and platform_system=='Windows'
6768
# On Mac, PyMsalRuntime 0.17+ is expected to support SSH cert and ROPC
68-
pymsalruntime>=0.20.6,<0.21; python_version>='3.8' and platform_system=='Darwin'
69+
pymsalruntime>=0.20,<0.21; python_version>='3.9' and platform_system=='Darwin'
6970
# PyMsalRuntime 0.18+ is expected to support broker on Linux
70-
pymsalruntime>=0.20.6,<0.21; python_version>='3.8' and platform_system=='Linux'
71+
pymsalruntime>=0.20,<0.21; python_version>='3.9' and platform_system=='Linux'
7172

7273
[options.packages.find]
7374
exclude =

tests/test_application.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
import sys
77
import time
8+
import warnings
89
from unittest.mock import patch, Mock
910
import msal
1011
from msal.application import (
@@ -708,6 +709,99 @@ def test_organizations_authority_should_emit_warning(self):
708709
authority="https://login.microsoftonline.com/organizations")
709710

710711

712+
@patch(_OIDC_DISCOVERY, new=_OIDC_DISCOVERY_MOCK)
713+
class TestClientAssertionCallback(unittest.TestCase):
714+
"""Issue #746: client_credential={'client_assertion': callable} support."""
715+
716+
_AUTHORITY = "https://login.microsoftonline.com/my_tenant"
717+
718+
def _mock_post_capturing(self, captured):
719+
def mock_post(url, headers=None, data=None, *args, **kwargs):
720+
captured.append(dict(data or {}))
721+
return MinimalResponse(
722+
status_code=200, text=json.dumps({
723+
"access_token": "an AT", "expires_in": 3600}))
724+
return mock_post
725+
726+
def test_callable_client_assertion_is_invoked_per_request(self):
727+
calls = {"n": 0}
728+
def assertion_cb():
729+
calls["n"] += 1
730+
return "assertion-{}".format(calls["n"])
731+
app = ConfidentialClientApplication(
732+
"client_id",
733+
client_credential={"client_assertion": assertion_cb},
734+
authority=self._AUTHORITY)
735+
captured = []
736+
app.acquire_token_for_client(
737+
["s1"], post=self._mock_post_capturing(captured))
738+
app.acquire_token_for_client(
739+
["s2"], post=self._mock_post_capturing(captured))
740+
self.assertEqual(2, calls["n"], "Callable should be called per request")
741+
self.assertEqual("assertion-1", captured[0]["client_assertion"])
742+
self.assertEqual("assertion-2", captured[1]["client_assertion"])
743+
self.assertEqual(
744+
"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
745+
captured[0]["client_assertion_type"])
746+
747+
def test_autorefresher_caches_assertion(self):
748+
from msal import AutoRefresher
749+
calls = {"n": 0}
750+
def assertion_cb():
751+
calls["n"] += 1
752+
return "static-assertion"
753+
app = ConfidentialClientApplication(
754+
"client_id",
755+
client_credential={
756+
"client_assertion": AutoRefresher(assertion_cb, expires_in=3600)},
757+
authority=self._AUTHORITY)
758+
captured = []
759+
app.acquire_token_for_client(
760+
["s1"], post=self._mock_post_capturing(captured))
761+
app.acquire_token_for_client(
762+
["s2"], post=self._mock_post_capturing(captured))
763+
self.assertEqual(
764+
1, calls["n"],
765+
"AutoRefresher should reuse the assertion within its lifetime")
766+
self.assertEqual("static-assertion", captured[0]["client_assertion"])
767+
self.assertEqual("static-assertion", captured[1]["client_assertion"])
768+
769+
def test_string_client_assertion_still_works_for_backward_compat(self):
770+
with warnings.catch_warnings():
771+
warnings.simplefilter("ignore", DeprecationWarning)
772+
app = ConfidentialClientApplication(
773+
"client_id",
774+
client_credential={"client_assertion": "static-jwt"},
775+
authority=self._AUTHORITY)
776+
captured = []
777+
result = app.acquire_token_for_client(
778+
["s"], post=self._mock_post_capturing(captured))
779+
self.assertEqual("an AT", result.get("access_token"))
780+
self.assertEqual("static-jwt", captured[0]["client_assertion"])
781+
782+
def test_string_client_assertion_emits_deprecation_warning(self):
783+
with self.assertWarns(DeprecationWarning):
784+
ConfidentialClientApplication(
785+
"client_id",
786+
client_credential={"client_assertion": "static-jwt"},
787+
authority=self._AUTHORITY)
788+
789+
def test_callable_client_assertion_does_not_emit_deprecation_warning(self):
790+
with warnings.catch_warnings(record=True) as caught:
791+
warnings.simplefilter("always")
792+
ConfidentialClientApplication(
793+
"client_id",
794+
client_credential={"client_assertion": lambda: "x"},
795+
authority=self._AUTHORITY)
796+
offending = [
797+
w for w in caught
798+
if issubclass(w.category, DeprecationWarning)
799+
and "client_assertion" in str(w.message)]
800+
self.assertEqual(
801+
[], offending,
802+
"Callable client_assertion must not emit a deprecation warning")
803+
804+
711805
@patch(_OIDC_DISCOVERY, new=_OIDC_DISCOVERY_MOCK)
712806
class TestAcquireTokenForClientWithFmiPath(unittest.TestCase):
713807
"""Test that acquire_token_for_client(fmi_path=...) attaches fmi_path to HTTP body."""

0 commit comments

Comments
 (0)