Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-azure-core for 
openSUSE:Factory checked in at 2026-03-17 19:02:48
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-azure-core (Old)
 and      /work/SRC/openSUSE:Factory/.python-azure-core.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-azure-core"

Tue Mar 17 19:02:48 2026 rev:63 rq:1339361 version:1.38.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-azure-core/python-azure-core.changes      
2026-02-21 21:01:10.798969851 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-azure-core.new.8177/python-azure-core.changes
    2026-03-17 19:04:10.439867202 +0100
@@ -1,0 +2,8 @@
+Mon Mar 16 13:02:53 UTC 2026 - John Paul Adrian Glaubitz 
<[email protected]>
+
+- New upstream release
+  + Version 1.38.3
+  + For detailed information about changes see the
+    CHANGELOG.md file provided with this package
+
+-------------------------------------------------------------------

Old:
----
  azure_core-1.38.2.tar.gz

New:
----
  azure_core-1.38.3.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-azure-core.spec ++++++
--- /var/tmp/diff_new_pack.FefOUH/_old  2026-03-17 19:04:11.063893063 +0100
+++ /var/tmp/diff_new_pack.FefOUH/_new  2026-03-17 19:04:11.067893228 +0100
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-azure-core
-Version:        1.38.2
+Version:        1.38.3
 Release:        0
 Summary:        Microsoft Azure Core Library for Python
 License:        MIT

++++++ azure_core-1.38.2.tar.gz -> azure_core-1.38.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/azure_core-1.38.2/CHANGELOG.md 
new/azure_core-1.38.3/CHANGELOG.md
--- old/azure_core-1.38.2/CHANGELOG.md  2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/CHANGELOG.md  2026-03-12 20:09:29.000000000 +0100
@@ -1,5 +1,16 @@
 # Release History
 
+## 1.38.3 (2026-03-12)
+
+### Bugs Fixed
+
+- Fixed `PipelineClient.format_url` to preserve trailing slash in the base URL 
when the URL template is query-string-only (e.g., `?key=value`). #45365
+- Fixed `SensitiveHeaderCleanupPolicy` to persist the `insecure_domain_change` 
flag across retries after a cross-domain redirect. #45518
+
+### Other Changes
+
+- Added jitter to token refresh timing in `BearerTokenCredentialPolicy` and 
`AsyncBearerTokenCredentialPolicy` to prevent simultaneous token refresh 
attempts across multiple processes. This helps mitigate the thundering herd 
problem during token refresh operations. #43720
+
 ## 1.38.2 (2026-02-18)
 
 ### Bugs Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/azure_core-1.38.2/PKG-INFO 
new/azure_core-1.38.3/PKG-INFO
--- old/azure_core-1.38.2/PKG-INFO      2026-02-18 03:29:53.482592800 +0100
+++ new/azure_core-1.38.3/PKG-INFO      2026-03-12 20:10:17.493318300 +0100
@@ -1,11 +1,10 @@
 Metadata-Version: 2.4
 Name: azure-core
-Version: 1.38.2
+Version: 1.38.3
 Summary: Microsoft Azure Core Library for Python
-Home-page: 
https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core
-Author: Microsoft Corporation
-Author-email: [email protected]
-License: MIT License
+Author-email: Microsoft Corporation <[email protected]>
+License-Expression: MIT
+Project-URL: Repository, 
https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core
 Keywords: azure,azure sdk
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Programming Language :: Python
@@ -16,7 +15,6 @@
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
-Classifier: License :: OSI Approved :: MIT License
 Requires-Python: >=3.9
 Description-Content-Type: text/markdown
 License-File: LICENSE
@@ -26,19 +24,7 @@
 Requires-Dist: aiohttp>=3.0; extra == "aio"
 Provides-Extra: tracing
 Requires-Dist: opentelemetry-api~=1.26; extra == "tracing"
-Dynamic: author
-Dynamic: author-email
-Dynamic: classifier
-Dynamic: description
-Dynamic: description-content-type
-Dynamic: home-page
-Dynamic: keywords
-Dynamic: license
 Dynamic: license-file
-Dynamic: provides-extra
-Dynamic: requires-dist
-Dynamic: requires-python
-Dynamic: summary
 
 
 # Azure Core shared client library for Python
@@ -320,9 +306,19 @@
 <!-- LINKS -->
 [package]: https://pypi.org/project/azure-core/
 
-
 # Release History
 
+## 1.38.3 (2026-03-12)
+
+### Bugs Fixed
+
+- Fixed `PipelineClient.format_url` to preserve trailing slash in the base URL 
when the URL template is query-string-only (e.g., `?key=value`). #45365
+- Fixed `SensitiveHeaderCleanupPolicy` to persist the `insecure_domain_change` 
flag across retries after a cross-domain redirect. #45518
+
+### Other Changes
+
+- Added jitter to token refresh timing in `BearerTokenCredentialPolicy` and 
`AsyncBearerTokenCredentialPolicy` to prevent simultaneous token refresh 
attempts across multiple processes. This helps mitigate the thundering herd 
problem during token refresh operations. #43720
+
 ## 1.38.2 (2026-02-18)
 
 ### Bugs Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/azure_core-1.38.2/azure/core/_version.py 
new/azure_core-1.38.3/azure/core/_version.py
--- old/azure_core-1.38.2/azure/core/_version.py        2026-02-18 
03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/azure/core/_version.py        2026-03-12 
20:09:29.000000000 +0100
@@ -9,4 +9,4 @@
 # regenerated.
 # --------------------------------------------------------------------------
 
-VERSION = "1.38.2"
+VERSION = "1.38.3"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/azure_core-1.38.2/azure/core/pipeline/policies/_authentication.py 
new/azure_core-1.38.3/azure/core/pipeline/policies/_authentication.py
--- old/azure_core-1.38.2/azure/core/pipeline/policies/_authentication.py       
2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/azure/core/pipeline/policies/_authentication.py       
2026-03-12 20:09:29.000000000 +0100
@@ -5,6 +5,7 @@
 # -------------------------------------------------------------------------
 import time
 import base64
+import random
 from typing import TYPE_CHECKING, Optional, TypeVar, MutableMapping, Any, 
Union, cast
 
 from azure.core.credentials import (
@@ -36,6 +37,38 @@
 HTTPResponseType = TypeVar("HTTPResponseType", HttpResponse, 
LegacyHttpResponse)
 HTTPRequestType = TypeVar("HTTPRequestType", HttpRequest, LegacyHttpRequest)
 
+DEFAULT_REFRESH_WINDOW_SECONDS = 300  # 5 minutes
+MAX_REFRESH_JITTER_SECONDS = 60  # 1 minute
+
+
+def _should_refresh_token(token: Optional[Union["AccessToken", 
"AccessTokenInfo"]], refresh_jitter: int) -> bool:
+    """Check if a new token is needed based on expiry and refresh logic.
+
+    :param token: The current token or None if no token exists
+    :type token: Optional[Union[~azure.core.credentials.AccessToken, 
~azure.core.credentials.AccessTokenInfo]]
+    :param int refresh_jitter: The jitter to apply to refresh timing
+    :return: True if a new token is needed, False otherwise
+    :rtype: bool
+    """
+    if not token:
+        return True
+
+    now = time.time()
+    if token.expires_on <= now:
+        return True
+
+    refresh_on = getattr(token, "refresh_on", None)
+
+    if refresh_on:
+        # Apply jitter, but ensure that adding it doesn't push the refresh 
time past the actual expiration.
+        # This is a safeguard, as refresh_on is typically well before 
expires_on.
+        effective_refresh_time = min(refresh_on + refresh_jitter, 
token.expires_on)
+        return effective_refresh_time <= now
+
+    time_until_expiry = token.expires_on - now
+    # Reduce refresh window by jitter to delay refresh and distribute load
+    return time_until_expiry < (DEFAULT_REFRESH_WINDOW_SECONDS - 
refresh_jitter)
+
 
 # pylint:disable=too-few-public-methods
 class _BearerTokenCredentialPolicyBase:
@@ -54,6 +87,7 @@
         self._credential = credential
         self._token: Optional[Union["AccessToken", "AccessTokenInfo"]] = None
         self._enable_cae: bool = kwargs.get("enable_cae", False)
+        self._refresh_jitter = 0
 
     @staticmethod
     def _enforce_https(request: PipelineRequest[HTTPRequestType]) -> None:
@@ -82,9 +116,7 @@
 
     @property
     def _need_new_token(self) -> bool:
-        now = time.time()
-        refresh_on = getattr(self._token, "refresh_on", None)
-        return not self._token or (refresh_on and refresh_on <= now) or 
self._token.expires_on - now < 300
+        return _should_refresh_token(self._token, self._refresh_jitter)
 
     def _get_token(self, *scopes: str, **kwargs: Any) -> Union["AccessToken", 
"AccessTokenInfo"]:
         if self._enable_cae:
@@ -108,6 +140,7 @@
         :param str scopes: The type of access needed.
         """
         self._token = self._get_token(*scopes, **kwargs)
+        self._refresh_jitter = random.randint(0, MAX_REFRESH_JITTER_SECONDS)
 
 
 class BearerTokenCredentialPolicy(_BearerTokenCredentialPolicyBase, 
HTTPPolicy[HTTPRequestType, HTTPResponseType]):
@@ -182,10 +215,6 @@
                     raise ex from 
HttpResponseError(response=response.http_response)
 
                 if request_authorized:
-                    # if we receive a challenge response, we retrieve a new 
token
-                    # which matches the new target. In this case, we don't 
want to remove
-                    # token from the request so clear the 
'insecure_domain_change' tag
-                    request.context.options.pop("insecure_domain_change", 
False)
                     try:
                         response = self.next.send(request)
                         self.on_response(request, response)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/azure_core-1.38.2/azure/core/pipeline/policies/_authentication_async.py 
new/azure_core-1.38.3/azure/core/pipeline/policies/_authentication_async.py
--- old/azure_core-1.38.2/azure/core/pipeline/policies/_authentication_async.py 
2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/azure/core/pipeline/policies/_authentication_async.py 
2026-03-12 20:09:29.000000000 +0100
@@ -3,7 +3,7 @@
 # Licensed under the MIT License. See LICENSE.txt in the project root for
 # license information.
 # -------------------------------------------------------------------------
-import time
+import random
 import base64
 from typing import Any, Awaitable, Optional, cast, TypeVar, Union
 
@@ -18,6 +18,8 @@
 from azure.core.pipeline.policies import AsyncHTTPPolicy
 from azure.core.pipeline.policies._authentication import (
     _BearerTokenCredentialPolicyBase,
+    _should_refresh_token,
+    MAX_REFRESH_JITTER_SECONDS,
 )
 from azure.core.pipeline.transport import (
     AsyncHttpResponse as LegacyAsyncHttpResponse,
@@ -50,6 +52,7 @@
         self._lock_instance = None
         self._token: Optional[Union["AccessToken", "AccessTokenInfo"]] = None
         self._enable_cae: bool = kwargs.get("enable_cae", False)
+        self._refresh_jitter = 0
 
     @property
     def _lock(self):
@@ -127,10 +130,6 @@
                     raise ex from 
HttpResponseError(response=response.http_response)
 
                 if request_authorized:
-                    # if we receive a challenge response, we retrieve a new 
token
-                    # which matches the new target. In this case, we don't 
want to remove
-                    # token from the request so clear the 
'insecure_domain_change' tag
-                    request.context.options.pop("insecure_domain_change", 
False)
                     try:
                         response = await self.next.send(request)
                     except Exception:
@@ -192,9 +191,7 @@
         return
 
     def _need_new_token(self) -> bool:
-        now = time.time()
-        refresh_on = getattr(self._token, "refresh_on", None)
-        return not self._token or (refresh_on and refresh_on <= now) or 
self._token.expires_on - now < 300
+        return _should_refresh_token(self._token, self._refresh_jitter)
 
     async def _get_token(self, *scopes: str, **kwargs: Any) -> 
Union["AccessToken", "AccessTokenInfo"]:
         if self._enable_cae:
@@ -226,3 +223,4 @@
         :param str scopes: The type of access needed.
         """
         self._token = await self._get_token(*scopes, **kwargs)
+        self._refresh_jitter = random.randint(0, MAX_REFRESH_JITTER_SECONDS)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/azure_core-1.38.2/azure/core/pipeline/policies/_redirect.py 
new/azure_core-1.38.3/azure/core/pipeline/policies/_redirect.py
--- old/azure_core-1.38.2/azure/core/pipeline/policies/_redirect.py     
2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/azure/core/pipeline/policies/_redirect.py     
2026-03-12 20:09:29.000000000 +0100
@@ -210,9 +210,8 @@
                 if domain_changed(original_domain, request.http_request.url):
                     # "insecure_domain_change" is used to indicate that a 
redirect
                     # has occurred to a different domain. This tells the 
SensitiveHeaderCleanupPolicy
-                    # to clean up sensitive headers. We need to remove it 
before sending the request
-                    # to the transport layer.
-                    request.context.options["insecure_domain_change"] = True
+                    # to clean up sensitive headers.
+                    request.context["insecure_domain_change"] = True
                 continue
             return response
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/azure_core-1.38.2/azure/core/pipeline/policies/_redirect_async.py 
new/azure_core-1.38.3/azure/core/pipeline/policies/_redirect_async.py
--- old/azure_core-1.38.2/azure/core/pipeline/policies/_redirect_async.py       
2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/azure/core/pipeline/policies/_redirect_async.py       
2026-03-12 20:09:29.000000000 +0100
@@ -81,9 +81,8 @@
                 if domain_changed(original_domain, request.http_request.url):
                     # "insecure_domain_change" is used to indicate that a 
redirect
                     # has occurred to a different domain. This tells the 
SensitiveHeaderCleanupPolicy
-                    # to clean up sensitive headers. We need to remove it 
before sending the request
-                    # to the transport layer.
-                    request.context.options["insecure_domain_change"] = True
+                    # to clean up sensitive headers.
+                    request.context["insecure_domain_change"] = True
                 continue
             return response
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/azure_core-1.38.2/azure/core/pipeline/policies/_sensitive_header_cleanup_policy.py
 
new/azure_core-1.38.3/azure/core/pipeline/policies/_sensitive_header_cleanup_policy.py
--- 
old/azure_core-1.38.2/azure/core/pipeline/policies/_sensitive_header_cleanup_policy.py
      2026-02-18 03:29:16.000000000 +0100
+++ 
new/azure_core-1.38.3/azure/core/pipeline/policies/_sensitive_header_cleanup_policy.py
      2026-03-12 20:09:29.000000000 +0100
@@ -72,9 +72,8 @@
         """
         # "insecure_domain_change" is used to indicate that a redirect
         # has occurred to a different domain. This tells the 
SensitiveHeaderCleanupPolicy
-        # to clean up sensitive headers. We need to remove it before sending 
the request
-        # to the transport layer.
-        insecure_domain_change = 
request.context.options.pop("insecure_domain_change", False)
+        # to clean up sensitive headers.
+        insecure_domain_change = request.context.get("insecure_domain_change", 
False)
         if not self._disable_redirect_cleanup and insecure_domain_change:
             for header in self._blocked_redirect_headers:
                 request.http_request.headers.pop(header, None)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/azure_core-1.38.2/azure/core/pipeline/transport/_base.py 
new/azure_core-1.38.3/azure/core/pipeline/transport/_base.py
--- old/azure_core-1.38.2/azure/core/pipeline/transport/_base.py        
2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/azure/core/pipeline/transport/_base.py        
2026-03-12 20:09:29.000000000 +0100
@@ -663,7 +663,7 @@
             parsed = urlparse(url)
             if not parsed.scheme or not parsed.netloc:
                 try:
-                    base = self._base_url.format(**kwargs).rstrip("/")
+                    base = self._base_url.format(**kwargs)
                 except KeyError as key:
                     err_msg = "The value provided for the url part {} was 
incorrect, and resulted in an invalid url"
                     raise ValueError(err_msg.format(key.args[0])) from key
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/azure_core-1.38.2/azure_core.egg-info/PKG-INFO 
new/azure_core-1.38.3/azure_core.egg-info/PKG-INFO
--- old/azure_core-1.38.2/azure_core.egg-info/PKG-INFO  2026-02-18 
03:29:53.000000000 +0100
+++ new/azure_core-1.38.3/azure_core.egg-info/PKG-INFO  2026-03-12 
20:10:17.000000000 +0100
@@ -1,11 +1,10 @@
 Metadata-Version: 2.4
 Name: azure-core
-Version: 1.38.2
+Version: 1.38.3
 Summary: Microsoft Azure Core Library for Python
-Home-page: 
https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core
-Author: Microsoft Corporation
-Author-email: [email protected]
-License: MIT License
+Author-email: Microsoft Corporation <[email protected]>
+License-Expression: MIT
+Project-URL: Repository, 
https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core
 Keywords: azure,azure sdk
 Classifier: Development Status :: 5 - Production/Stable
 Classifier: Programming Language :: Python
@@ -16,7 +15,6 @@
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
-Classifier: License :: OSI Approved :: MIT License
 Requires-Python: >=3.9
 Description-Content-Type: text/markdown
 License-File: LICENSE
@@ -26,19 +24,7 @@
 Requires-Dist: aiohttp>=3.0; extra == "aio"
 Provides-Extra: tracing
 Requires-Dist: opentelemetry-api~=1.26; extra == "tracing"
-Dynamic: author
-Dynamic: author-email
-Dynamic: classifier
-Dynamic: description
-Dynamic: description-content-type
-Dynamic: home-page
-Dynamic: keywords
-Dynamic: license
 Dynamic: license-file
-Dynamic: provides-extra
-Dynamic: requires-dist
-Dynamic: requires-python
-Dynamic: summary
 
 
 # Azure Core shared client library for Python
@@ -320,9 +306,19 @@
 <!-- LINKS -->
 [package]: https://pypi.org/project/azure-core/
 
-
 # Release History
 
+## 1.38.3 (2026-03-12)
+
+### Bugs Fixed
+
+- Fixed `PipelineClient.format_url` to preserve trailing slash in the base URL 
when the URL template is query-string-only (e.g., `?key=value`). #45365
+- Fixed `SensitiveHeaderCleanupPolicy` to persist the `insecure_domain_change` 
flag across retries after a cross-domain redirect. #45518
+
+### Other Changes
+
+- Added jitter to token refresh timing in `BearerTokenCredentialPolicy` and 
`AsyncBearerTokenCredentialPolicy` to prevent simultaneous token refresh 
attempts across multiple processes. This helps mitigate the thundering herd 
problem during token refresh operations. #43720
+
 ## 1.38.2 (2026-02-18)
 
 ### Bugs Fixed
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/azure_core-1.38.2/azure_core.egg-info/SOURCES.txt 
new/azure_core-1.38.3/azure_core.egg-info/SOURCES.txt
--- old/azure_core-1.38.2/azure_core.egg-info/SOURCES.txt       2026-02-18 
03:29:53.000000000 +0100
+++ new/azure_core-1.38.3/azure_core.egg-info/SOURCES.txt       2026-03-12 
20:10:17.000000000 +0100
@@ -5,7 +5,6 @@
 README.md
 TROUBLESHOOTING.md
 pyproject.toml
-setup.py
 azure/__init__.py
 azure/core/__init__.py
 azure/core/_azure_clouds.py
@@ -85,7 +84,6 @@
 azure_core.egg-info/PKG-INFO
 azure_core.egg-info/SOURCES.txt
 azure_core.egg-info/dependency_links.txt
-azure_core.egg-info/not-zip-safe
 azure_core.egg-info/requires.txt
 azure_core.egg-info/top_level.txt
 doc/azure.core.pipeline.policies.rst
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/azure_core-1.38.2/azure_core.egg-info/not-zip-safe 
new/azure_core-1.38.3/azure_core.egg-info/not-zip-safe
--- old/azure_core-1.38.2/azure_core.egg-info/not-zip-safe      2026-02-18 
03:29:53.000000000 +0100
+++ new/azure_core-1.38.3/azure_core.egg-info/not-zip-safe      1970-01-01 
01:00:00.000000000 +0100
@@ -1 +0,0 @@
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/azure_core-1.38.2/pyproject.toml 
new/azure_core-1.38.3/pyproject.toml
--- old/azure_core-1.38.2/pyproject.toml        2026-02-18 03:29:16.000000000 
+0100
+++ new/azure_core-1.38.3/pyproject.toml        2026-03-12 20:09:29.000000000 
+0100
@@ -1,3 +1,50 @@
+[build-system]
+requires = ["setuptools>=77.0.3", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "azure-core"
+authors = [
+    {name = "Microsoft Corporation", email = "[email protected]"},
+]
+description = "Microsoft Azure Core Library for Python"
+keywords = ["azure", "azure sdk"]
+requires-python = ">=3.9"
+license = "MIT"
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    "Programming Language :: Python",
+    "Programming Language :: Python :: 3 :: Only",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: 3.10",
+    "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
+]
+dependencies = [
+    "requests>=2.21.0",
+    "typing-extensions>=4.6.0",
+]
+dynamic = ["version", "readme"]
+
+[project.optional-dependencies]
+aio = ["aiohttp>=3.0"]
+tracing = ["opentelemetry-api~=1.26"]
+
+[project.urls]
+Repository = 
"https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core";
+
+[tool.setuptools.dynamic]
+version = {attr = "azure.core._version.VERSION"}
+readme = {file = ["README.md", "CHANGELOG.md"], content-type = "text/markdown"}
+
+[tool.setuptools.packages.find]
+include = ["azure.core*"]
+
+[tool.setuptools.package-data]
+pytyped = ["py.typed"]
+
 [tool.azure-sdk-build]
 mypy = true
 type_check_samples = true
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/azure_core-1.38.2/setup.py 
new/azure_core-1.38.3/setup.py
--- old/azure_core-1.38.2/setup.py      2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/setup.py      1970-01-01 01:00:00.000000000 +0100
@@ -1,83 +0,0 @@
-#!/usr/bin/env python
-
-# -------------------------------------------------------------------------
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License. See License.txt in the project root for
-# license information.
-# --------------------------------------------------------------------------
-
-import re
-import os.path
-from io import open
-from setuptools import find_packages, setup  # type: ignore
-
-# Change the PACKAGE_NAME only to change folder and different name
-PACKAGE_NAME = "azure-core"
-PACKAGE_PPRINT_NAME = "Core"
-
-# a-b-c => a/b/c
-package_folder_path = PACKAGE_NAME.replace("-", "/")
-# a-b-c => a.b.c
-namespace_name = PACKAGE_NAME.replace("-", ".")
-
-# Version extraction inspired from 'requests'
-with open(os.path.join(package_folder_path, "_version.py"), "r") as fd:
-    version = re.search(r'^VERSION\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), 
re.MULTILINE).group(1)  # type: ignore
-
-if not version:
-    raise RuntimeError("Cannot find version information")
-
-with open("README.md", encoding="utf-8") as f:
-    readme = f.read()
-with open("CHANGELOG.md", encoding="utf-8") as f:
-    changelog = f.read()
-
-setup(
-    name=PACKAGE_NAME,
-    version=version,
-    include_package_data=True,
-    description="Microsoft Azure {} Library for 
Python".format(PACKAGE_PPRINT_NAME),
-    long_description=readme + "\n\n" + changelog,
-    long_description_content_type="text/markdown",
-    license="MIT License",
-    author="Microsoft Corporation",
-    author_email="[email protected]",
-    
url="https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/core/azure-core";,
-    keywords="azure, azure sdk",
-    classifiers=[
-        "Development Status :: 5 - Production/Stable",
-        "Programming Language :: Python",
-        "Programming Language :: Python :: 3 :: Only",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.9",
-        "Programming Language :: Python :: 3.10",
-        "Programming Language :: Python :: 3.11",
-        "Programming Language :: Python :: 3.12",
-        "Programming Language :: Python :: 3.13",
-        "License :: OSI Approved :: MIT License",
-    ],
-    zip_safe=False,
-    packages=find_packages(
-        exclude=[
-            "tests",
-            # Exclude packages that will be covered by PEP420 or nspkg
-            "azure",
-        ]
-    ),
-    package_data={
-        "pytyped": ["py.typed"],
-    },
-    python_requires=">=3.9",
-    install_requires=[
-        "requests>=2.21.0",
-        "typing-extensions>=4.6.0",
-    ],
-    extras_require={
-        "aio": [
-            "aiohttp>=3.0",
-        ],
-        "tracing": [
-            "opentelemetry-api~=1.26",
-        ],
-    },
-)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/azure_core-1.38.2/tests/async_tests/test_authentication_async.py 
new/azure_core-1.38.3/tests/async_tests/test_authentication_async.py
--- old/azure_core-1.38.2/tests/async_tests/test_authentication_async.py        
2026-02-18 03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/tests/async_tests/test_authentication_async.py        
2026-03-12 20:09:29.000000000 +0100
@@ -18,8 +18,10 @@
     AsyncBearerTokenCredentialPolicy,
     SansIOHTTPPolicy,
     AsyncRedirectPolicy,
+    AsyncRetryPolicy,
     SensitiveHeaderCleanupPolicy,
 )
+from azure.core.pipeline.policies._authentication import 
MAX_REFRESH_JITTER_SECONDS
 from azure.core.pipeline.transport import AsyncHttpTransport, HttpRequest
 import pytest
 import trio
@@ -244,7 +246,9 @@
     await pipeline.run(http_request("GET", "https://spam.eggs";))
     assert credential.get_token_info.call_count == 2  # token is expired -> 
policy should call get_token_info again
 
-    refreshable_token = AccessTokenInfo("token", int(time.time() + 3600), 
refresh_on=int(time.time() - 1))
+    refreshable_token = AccessTokenInfo(
+        "token", int(time.time() + 3600), refresh_on=int(time.time() - 
(MAX_REFRESH_JITTER_SECONDS + 5))
+    )
     credential.get_token_info.reset_mock()
     credential.get_token_info.return_value = refreshable_token
     pipeline = AsyncPipeline(transport=AsyncMock(), 
policies=[AsyncBearerTokenCredentialPolicy(credential, "scope")])
@@ -735,3 +739,171 @@
     # Verify the exception chaining
     assert exc_info.value.__cause__ is not None
     assert isinstance(exc_info.value.__cause__, HttpResponseError)
+
+
[email protected]
+async def test_jitter_set_on_token_request_async():
+    """Test that _refresh_jitter is set when _request_token is called on the 
async policy."""
+    token = AccessToken("test_token", int(time.time()) + 3600)
+
+    credential = AsyncMock(spec_set=["get_token"])
+    credential.get_token.return_value = token
+    policy = AsyncBearerTokenCredentialPolicy(credential, "scope")
+
+    # Initially jitter should be 0
+    assert policy._refresh_jitter == 0
+
+    with 
patch("azure.core.pipeline.policies._authentication_async.random.randint") as 
mock_randint:
+        mock_randint.return_value = 42
+
+        await policy._request_token("scope")
+
+        assert policy._refresh_jitter == 42
+        mock_randint.assert_called_once_with(0, MAX_REFRESH_JITTER_SECONDS)
+
+    # Test that jitter is updated on subsequent token requests
+    with 
patch("azure.core.pipeline.policies._authentication_async.random.randint") as 
mock_randint:
+        mock_randint.return_value = 25
+
+        await policy._request_token("scope")
+
+        assert policy._refresh_jitter == 25
+        mock_randint.assert_called_once_with(0, MAX_REFRESH_JITTER_SECONDS)
+
+
[email protected]
+async def test_challenge_auth_header_stripped_after_redirect():
+    """Assuming the SensitiveHeaderCleanupPolicy is in the pipeline, the 
authorization header should be stripped after
+    a redirect to a different domain by default, and preserved if the policy 
is configured to disable cleanup."""
+
+    class MockTransport(AsyncHttpTransport):
+        def __init__(self, cleanup_disabled=False):
+            self._first = True
+            self._cleanup_disabled = cleanup_disabled
+
+        async def __aexit__(self, exc_type, exc_val, exc_tb):
+            pass
+
+        async def close(self):
+            pass
+
+        async def open(self):
+            pass
+
+        async def send(self, request, **kwargs):
+            if self._first:
+                self._first = False
+                assert request.headers["Authorization"] == "Bearer 
{}".format(auth_header)
+                response = Response()
+                response.status_code = 307
+                response.headers["location"] = 
"https://redirect-target.example.invalid";
+                return response
+
+            # Second request: after redirect
+            if self._cleanup_disabled:
+                assert request.headers.get("Authorization")
+            else:
+                assert not request.headers.get("Authorization")
+            response = Response()
+            response.status_code = 401
+            response.headers["WWW-Authenticate"] = (
+                'Bearer error="insufficient_claims", 
claims="eyJhY2Nlc3NfdG9rZW4iOnsiZm9vIjoiYmFyIn19"'
+            )
+            return response
+
+    auth_header = "token"
+    get_token_call_count = 0
+
+    async def mock_get_token(*_, **__):
+        nonlocal get_token_call_count
+        get_token_call_count += 1
+        return AccessToken(auth_header, 0)
+
+    credential = Mock(spec_set=["get_token"], get_token=mock_get_token)
+    auth_policy = AsyncBearerTokenCredentialPolicy(credential, "scope")
+    redirect_policy = AsyncRedirectPolicy()
+    header_clean_up_policy = SensitiveHeaderCleanupPolicy()
+    pipeline = AsyncPipeline(transport=MockTransport(), 
policies=[redirect_policy, auth_policy, header_clean_up_policy])
+
+    response = await pipeline.run(HttpRequest("GET", 
"https://legitimate.azure.com";))
+    assert response.http_response.status_code == 401
+
+    header_clean_up_policy = 
SensitiveHeaderCleanupPolicy(disable_redirect_cleanup=True)
+    pipeline = AsyncPipeline(
+        transport=MockTransport(cleanup_disabled=True),
+        policies=[redirect_policy, auth_policy, header_clean_up_policy],
+    )
+    response = await pipeline.run(HttpRequest("GET", 
"https://legitimate.azure.com";))
+    assert response.http_response.status_code == 401
+
+
[email protected]
+async def test_auth_header_stripped_after_cross_domain_redirect_with_retry():
+    """After a cross-domain redirect, if the redirected-to endpoint returns a 
retryable status code,
+    the Authorization header should still be stripped on the retry attempt. 
This verifies that the
+    insecure_domain_change flag persists across retries so 
SensitiveHeaderCleanupPolicy continues to
+    remove the Authorization header."""
+
+    class MockTransport(AsyncHttpTransport):
+        def __init__(self):
+            self._request_count = 0
+
+        async def __aexit__(self, exc_type, exc_val, exc_tb):
+            pass
+
+        async def close(self):
+            pass
+
+        async def open(self):
+            pass
+
+        async def send(self, request, **kwargs):
+            self._request_count += 1
+
+            if self._request_count == 1:
+                # First request: to the original domain — should have auth 
header
+                assert request.headers.get("Authorization") == "Bearer 
{}".format(auth_header)
+                response = Response()
+                response.status_code = 307
+                response.headers["location"] = 
"https://redirect-target.example.invalid";
+                return response
+
+            if self._request_count == 2:
+                # Second request: after redirect to attacker domain — auth 
header should be stripped
+                assert not request.headers.get(
+                    "Authorization"
+                ), "Authorization header should be stripped on first request 
to redirected domain"
+                response = Response()
+                response.status_code = 500
+                return response
+
+            if self._request_count == 3:
+                # Third request: retry to attacker domain — auth header should 
STILL be stripped
+                assert not request.headers.get(
+                    "Authorization"
+                ), "Authorization header should be stripped on retry to 
redirected domain"
+                response = Response()
+                response.status_code = 200
+                return response
+
+            raise RuntimeError("Unexpected request count: 
{}".format(self._request_count))
+
+    auth_header = "token"
+
+    async def mock_get_token(*_, **__):
+        return AccessToken(auth_header, 0)
+
+    credential = Mock(spec_set=["get_token"], get_token=mock_get_token)
+    auth_policy = AsyncBearerTokenCredentialPolicy(credential, "scope")
+    redirect_policy = AsyncRedirectPolicy()
+    retry_policy = AsyncRetryPolicy(retry_total=1, retry_backoff_factor=0)
+    header_clean_up_policy = SensitiveHeaderCleanupPolicy()
+    transport = MockTransport()
+    # Pipeline order matches the real default: redirect -> retry -> auth -> 
... -> sensitive header cleanup
+    pipeline = AsyncPipeline(
+        transport=transport,
+        policies=[redirect_policy, retry_policy, auth_policy, 
header_clean_up_policy],
+    )
+    response = await pipeline.run(HttpRequest("GET", 
"https://legitimate.azure.com";))
+    assert response.http_response.status_code == 200
+    assert transport._request_count == 3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/azure_core-1.38.2/tests/test_authentication.py 
new/azure_core-1.38.3/tests/test_authentication.py
--- old/azure_core-1.38.2/tests/test_authentication.py  2026-02-18 
03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/tests/test_authentication.py  2026-03-12 
20:09:29.000000000 +0100
@@ -22,16 +22,22 @@
 from azure.core.pipeline.policies import (
     BearerTokenCredentialPolicy,
     RedirectPolicy,
+    RetryPolicy,
     SansIOHTTPPolicy,
     AzureKeyCredentialPolicy,
     AzureSasCredentialPolicy,
     SensitiveHeaderCleanupPolicy,
 )
+from azure.core.pipeline.policies._authentication import (
+    DEFAULT_REFRESH_WINDOW_SECONDS,
+    MAX_REFRESH_JITTER_SECONDS,
+    _should_refresh_token,
+)
 from utils import HTTP_REQUESTS
 
 import pytest
 
-from unittest.mock import Mock
+from unittest.mock import Mock, patch
 
 
 @pytest.mark.parametrize("http_request", HTTP_REQUESTS)
@@ -193,7 +199,9 @@
     pipeline.run(http_request("GET", "https://spam.eggs";))
     assert credential.get_token_info.call_count == 2  # token is expired -> 
policy should call get_token_info again
 
-    refreshable_token = AccessTokenInfo("token", int(time.time() + 3600), 
refresh_on=int(time.time() - 1))
+    refreshable_token = AccessTokenInfo(
+        "token", int(time.time() + 3600), refresh_on=int(time.time() - 
(MAX_REFRESH_JITTER_SECONDS + 5))
+    )
     credential.get_token_info.reset_mock()
     credential.get_token_info.return_value = refreshable_token
     pipeline = Pipeline(transport=Mock(), 
policies=[BearerTokenCredentialPolicy(credential, "scope")])
@@ -417,33 +425,187 @@
 
 
 def test_need_new_token():
-    expected_scope = "scope"
     now = int(time.time())
 
-    policy = BearerTokenCredentialPolicy(Mock(), expected_scope)
-
     # Token is expired.
-    policy._token = AccessToken("", now - 1200)
-    assert policy._need_new_token
+    token = AccessToken("", now - 1200)
+    assert _should_refresh_token(token, 0)
 
     # Token is about to expire within 300 seconds.
-    policy._token = AccessToken("", now + 299)
-    assert policy._need_new_token
+    token = AccessToken("", now + 299)
+    assert _should_refresh_token(token, 0)
 
     # Token still has more than 300 seconds to live.
-    policy._token = AccessToken("", now + 305)
-    assert not policy._need_new_token
+    token = AccessToken("", now + 305)
+    assert not _should_refresh_token(token, 0)
 
     # Token has both expires_on and refresh_on set well into the future.
-    policy._token = AccessTokenInfo("", now + 1200, refresh_on=now + 1200)
-    assert not policy._need_new_token
+    token = AccessTokenInfo("", now + 1200, refresh_on=now + 1200)
+    assert not _should_refresh_token(token, 0)
 
     # Token is not close to expiring, but refresh_on is in the past.
-    policy._token = AccessTokenInfo("", now + 1200, refresh_on=now - 1)
-    assert policy._need_new_token
+    token = AccessTokenInfo("", now + 1200, refresh_on=now - 1)
+    assert _should_refresh_token(token, 0)
 
-    policy._token = None
-    assert policy._need_new_token
+    # No token
+    assert _should_refresh_token(None, 0)
+
+
+def test_need_new_token_with_jitter():
+    """Test that jitter affects token refresh timing for both expires_on and 
refresh_on scenarios."""
+    # Mock time.time() to have precise control over timing
+    with patch("azure.core.pipeline.policies._authentication.time") as 
mock_time:
+        mock_time.time.return_value = 1000.0  # Fixed current time
+
+        # Test jitter with expires_on based tokens (no refresh_on)
+        # Test with a token that expires in 290 seconds (would normally 
trigger refresh)
+        token = AccessToken("", 1290)  # 1000 + 290
+
+        # Set jitter to 0 - should need new token (290 < 
DEFAULT_REFRESH_WINDOW_SECONDS)
+        assert _should_refresh_token(token, 0)
+
+        # Set jitter to 10 - should NOT need new token (290 < 290 is False)
+        assert not _should_refresh_token(token, 10)
+
+        # Test with a token that expires in 250 seconds
+        token = AccessToken("", 1250)  # 1000 + 250
+
+        # With jitter of 0 - should need new token (250 < 
DEFAULT_REFRESH_WINDOW_SECONDS)
+        assert _should_refresh_token(token, 0)
+
+        # With jitter of 10 - should need new token (250 < 290)
+        assert _should_refresh_token(token, 10)
+
+        # With max jitter (60) - should NOT need new token (250 < 240 is False)
+        assert not _should_refresh_token(token, MAX_REFRESH_JITTER_SECONDS)
+
+        # Test with a token that expires in 200 seconds
+        token = AccessToken("", 1200)  # 1000 + 200
+
+        # Even with max jitter, should need new token (200 < 240)
+        assert _should_refresh_token(token, MAX_REFRESH_JITTER_SECONDS)
+
+
+def test_need_new_token_with_refresh_on_and_jitter():
+    """Test that jitter affects refresh_on based token refresh timing."""
+    with patch("azure.core.pipeline.policies._authentication.time") as 
mock_time:
+        mock_time.time.return_value = 1000.0  # Fixed current time
+
+        # Test jitter with refresh_on based tokens
+        # Token expires in 1 hour but should refresh in 5 minutes
+        refresh_time = 1300  # 1000 + 300
+        expiry_time = 4600  # 1000 + 3600
+        token = AccessTokenInfo("", expiry_time, refresh_on=refresh_time)
+
+        # Set jitter to 0 - effective refresh time is 1300, now=1000, so no 
refresh needed
+        assert not _should_refresh_token(token, 0)
+
+        # Set jitter to 30 - effective refresh time is 1330, now=1000, so no 
refresh needed
+        assert not _should_refresh_token(token, 30)
+
+        # Test with refresh_on in the past
+        token = AccessTokenInfo("", expiry_time, refresh_on=990)  # refresh_on 
= 1000 - 10
+
+        # With jitter of 5, effective refresh time is min(990 + 5, 4600) = 
995, still < 1000
+        assert _should_refresh_token(token, 5)
+
+        # With jitter of 15, effective refresh time is min(990 + 15, 4600) = 
1005, now < 1005 so no refresh
+        assert not _should_refresh_token(token, 15)
+
+        # Test jitter capping at expires_on
+        # Token expires soon but refresh_on is very close to expires_on
+        close_expiry = 1030  # 1000 + 30
+        token = AccessTokenInfo("", close_expiry, refresh_on=close_expiry - 5)
+
+        # Large jitter that would normally push refresh time past expiry
+        # Effective refresh time should be capped at expires_on
+        # min(1025 + 60, 1030) = 1030, now=1000 < 1030, so no refresh
+        assert not _should_refresh_token(token, MAX_REFRESH_JITTER_SECONDS)
+
+        # But if we're right at the expiry time, we should need a new token
+        token = AccessTokenInfo("", 1000, refresh_on=995)  # expires_on == now
+        # Effective refresh time: min(995 + 60, 1000) = 1000, now=1000 >= 1000
+        assert _should_refresh_token(token, MAX_REFRESH_JITTER_SECONDS)
+
+
+def test_need_new_token_jitter_boundary_conditions():
+    """Test boundary conditions for jitter in token refresh logic."""
+    # Mock time.time() to have precise control over timing
+    with patch("azure.core.pipeline.policies._authentication.time") as 
mock_time:
+        mock_time.time.return_value = 1000.0  # Fixed current time
+
+        # Test boundary at DEFAULT_REFRESH_WINDOW_SECONDS (the default refresh 
window)
+        token = AccessToken("", 1000 + DEFAULT_REFRESH_WINDOW_SECONDS)
+
+        # Zero jitter - exactly at refresh window boundary, not less than, so 
no refresh
+        assert not _should_refresh_token(token, 0)
+
+        # Test at 1 second under the refresh window
+        token = AccessToken("", 1000 + DEFAULT_REFRESH_WINDOW_SECONDS - 1)
+
+        # Zero jitter - 299 seconds remaining, 299 < 
DEFAULT_REFRESH_WINDOW_SECONDS = True (refresh)
+        assert _should_refresh_token(token, 0)
+
+        # Small jitter - 299 seconds remaining, 299 < 295 = False (no refresh)
+        assert not _should_refresh_token(token, 5)
+
+        # Test at 250 seconds
+        token = AccessToken("", 1250)  # 1000 + 250
+
+        # Zero jitter - 250 seconds remaining, 250 < 
DEFAULT_REFRESH_WINDOW_SECONDS = True (refresh)
+        assert _should_refresh_token(token, 0)
+
+        # Small jitter - 250 seconds remaining, 250 < 290 = True (refresh)
+        assert _should_refresh_token(token, 10)
+
+        # Max jitter - 250 seconds remaining, 250 < 240 = False (no refresh)
+        assert not _should_refresh_token(token, MAX_REFRESH_JITTER_SECONDS)
+
+        # Test refresh_on scenarios with jitter
+        # Test refresh_on exactly at current time
+        token = AccessTokenInfo("", 4600, refresh_on=1000)  # expires at 1000 
+ 3600, refresh at 1000
+        # Effective refresh time is 1000 + 0 = 1000, so 1000 >= 1000 is True
+        assert _should_refresh_token(token, 0)
+
+        # Test refresh_on with positive jitter (delays refresh)
+        token = AccessTokenInfo("", 4600, refresh_on=995)  # expires at 1000 + 
3600, refresh at 995
+        # Effective refresh time is min(995 + 10, 4600) = 1005, so 1000 < 1005 
(no refresh)
+        assert not _should_refresh_token(token, 10)
+
+
+def test_jitter_set_on_token_request():
+    """Test that _refresh_jitter is set when _request_token is called."""
+    expected_scope = "scope"
+    token = AccessToken("test_token", int(time.time()) + 3600)
+
+    # Mock credential that returns a token
+    credential = Mock(spec_set=["get_token"], 
get_token=Mock(return_value=token))
+    policy = BearerTokenCredentialPolicy(credential, expected_scope)
+
+    # Initially jitter should be 0
+    assert policy._refresh_jitter == 0
+
+    # Mock random.randint to return a known value
+    with patch("azure.core.pipeline.policies._authentication.random.randint") 
as mock_randint:
+        mock_randint.return_value = 42
+
+        # Request a token
+        policy._request_token(expected_scope)
+
+        # Verify jitter was set
+        assert policy._refresh_jitter == 42
+        mock_randint.assert_called_once_with(0, 60)  # 
MAX_REFRESH_JITTER_SECONDS
+
+    # Test that jitter is updated on subsequent token requests
+    with patch("azure.core.pipeline.policies._authentication.random.randint") 
as mock_randint:
+        mock_randint.return_value = 25
+
+        # Request another token
+        policy._request_token(expected_scope)
+
+        # Verify jitter was updated
+        assert policy._refresh_jitter == 25
+        mock_randint.assert_called_once_with(0, 60)
 
 
 def test_need_new_token_with_external_defined_token_class():
@@ -927,3 +1089,134 @@
     # Verify the exception chaining
     assert exc_info.value.__cause__ is not None
     assert isinstance(exc_info.value.__cause__, HttpResponseError)
+
+
+def test_challenge_auth_header_stripped_after_redirect():
+    """Assuming the SensitiveHeaderCleanupPolicy is in the pipeline, the 
authorization header should be stripped after
+    a redirect to a different domain by default, and preserved if the policy 
is configured to disable cleanup."""
+
+    class MockTransport(HttpTransport):
+        def __init__(self, cleanup_disabled=False):
+            self._first = True
+            self._cleanup_disabled = cleanup_disabled
+
+        def __exit__(self, exc_type, exc_val, exc_tb):
+            pass
+
+        def close(self):
+            pass
+
+        def open(self):
+            pass
+
+        def send(self, request, **kwargs):
+            if self._first:
+                self._first = False
+                assert request.headers["Authorization"] == "Bearer 
{}".format(auth_header)
+                response = Response()
+                response.status_code = 307
+                response.headers["location"] = 
"https://redirect-target.example.invalid";
+                return response
+
+            # Second request: after redirect
+            if self._cleanup_disabled:
+                assert request.headers.get("Authorization")
+            else:
+                assert not request.headers.get("Authorization")
+            response = Response()
+            response.status_code = 401
+            response.headers["WWW-Authenticate"] = (
+                'Bearer error="insufficient_claims", 
claims="eyJhY2Nlc3NfdG9rZW4iOnsiZm9vIjoiYmFyIn19"'
+            )
+            return response
+
+    auth_header = "token"
+    get_token_call_count = 0
+
+    def mock_get_token(*_, **__):
+        nonlocal get_token_call_count
+        get_token_call_count += 1
+        return AccessToken(auth_header, 0)
+
+    credential = Mock(spec_set=["get_token"], get_token=mock_get_token)
+    auth_policy = BearerTokenCredentialPolicy(credential, "scope")
+    redirect_policy = RedirectPolicy()
+    header_clean_up_policy = SensitiveHeaderCleanupPolicy()
+    pipeline = Pipeline(transport=MockTransport(), policies=[redirect_policy, 
auth_policy, header_clean_up_policy])
+    response = pipeline.run(HttpRequest("GET", "https://legitimate.azure.com";))
+    assert response.http_response.status_code == 401
+
+    header_clean_up_policy = 
SensitiveHeaderCleanupPolicy(disable_redirect_cleanup=True)
+    pipeline = Pipeline(
+        transport=MockTransport(cleanup_disabled=True), 
policies=[redirect_policy, auth_policy, header_clean_up_policy]
+    )
+    response = pipeline.run(HttpRequest("GET", "https://legitimate.azure.com";))
+    assert response.http_response.status_code == 401
+
+
+def test_auth_header_stripped_after_cross_domain_redirect_with_retry():
+    """After a cross-domain redirect, if the redirected-to endpoint returns a 
retryable status code,
+    the Authorization header should still be stripped on the retry attempt. 
This verifies that the
+    insecure_domain_change flag persists across retries so 
SensitiveHeaderCleanupPolicy continues to
+    remove the Authorization header."""
+
+    class MockTransport(HttpTransport):
+        def __init__(self):
+            self._request_count = 0
+
+        def __exit__(self, exc_type, exc_val, exc_tb):
+            pass
+
+        def close(self):
+            pass
+
+        def open(self):
+            pass
+
+        def send(self, request, **kwargs):
+            self._request_count += 1
+
+            if self._request_count == 1:
+                # First request: to the original domain — should have auth 
header
+                assert request.headers.get("Authorization") == "Bearer 
{}".format(auth_header)
+                response = Response()
+                response.status_code = 307
+                response.headers["location"] = 
"https://redirect-target.example.invalid";
+                return response
+
+            if self._request_count == 2:
+                # Second request: after redirect to attacker domain — auth 
header should be stripped
+                assert not request.headers.get(
+                    "Authorization"
+                ), "Authorization header should be stripped on first request 
to redirected domain"
+                response = Response()
+                response.status_code = 500
+                return response
+
+            if self._request_count == 3:
+                # Third request: retry to attacker domain — auth header should 
STILL be stripped
+                assert not request.headers.get(
+                    "Authorization"
+                ), "Authorization header should be stripped on retry to 
redirected domain"
+                response = Response()
+                response.status_code = 200
+                return response
+
+            raise RuntimeError("Unexpected request count: 
{}".format(self._request_count))
+
+    auth_header = "token"
+    token = AccessToken(auth_header, 0)
+    credential = Mock(spec_set=["get_token"], 
get_token=Mock(return_value=token))
+    auth_policy = BearerTokenCredentialPolicy(credential, "scope")
+    redirect_policy = RedirectPolicy()
+    retry_policy = RetryPolicy(retry_total=1, retry_backoff_factor=0)
+    header_clean_up_policy = SensitiveHeaderCleanupPolicy()
+    transport = MockTransport()
+    # Pipeline order matches the real default: redirect -> retry -> auth -> 
... -> sensitive header cleanup
+    pipeline = Pipeline(
+        transport=transport,
+        policies=[redirect_policy, retry_policy, auth_policy, 
header_clean_up_policy],
+    )
+    response = pipeline.run(HttpRequest("GET", "https://legitimate.azure.com";))
+    assert response.http_response.status_code == 200
+    assert transport._request_count == 3
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/azure_core-1.38.2/tests/test_pipeline.py 
new/azure_core-1.38.3/tests/test_pipeline.py
--- old/azure_core-1.38.2/tests/test_pipeline.py        2026-02-18 
03:29:16.000000000 +0100
+++ new/azure_core-1.38.3/tests/test_pipeline.py        2026-03-12 
20:09:29.000000000 +0100
@@ -225,6 +225,14 @@
     assert formatted == "https://foo.core.windows.net/Tables?a=X&c=Y";
 
 
+def test_format_url_trailing_slash_preserved_with_query_only():
+    # Test that trailing slash in base URL is preserved when url_template is 
query-string only
+    # https://github.com/Azure/azure-sdk-for-python/issues/45365
+    client = PipelineClientBase("{url}")
+    formatted = client.format_url("?versionid=2026-02-25", 
url="https://storage.blob.core.windows.net/sample//a/a/";)
+    assert formatted == 
"https://storage.blob.core.windows.net/sample//a/a/?versionid=2026-02-25";
+
+
 def test_format_url_from_http_request():
     client = PipelineClientBase("https://foo.core.windows.net";)
 

Reply via email to