[KeyVault] ChallengeAuthPolicy keeps the first request and re-sends it in place of a later request on any 401 challenge
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
调研方向
Read azure/keyvault/certificates/_shared/async_challenge_auth_policy.py and its sync twin, challenge_auth_policy.py, focusing on the request copy and challenge handling described in the issue. Add a regression test using the offline recording transport scenario, then run the relevant Key Vault certificate policy tests. Done means a later challenged GET is retried as that GET, while the saved body request is used only for its own challenge round.
由索引模型根据 Issue 内容生成。
描述
Package versions: azure-keyvault-certificates 4.11.2, azure-core 1.40.0, Python 3.14.6 (also read in the sync policy).
Describe the bug
The Key Vault challenge policy (azure/keyvault/certificates/_shared/async_challenge_auth_policy.py; the sync twin challenge_auth_policy.py has the same logic) keeps a copy of the first body-carrying request it sees while it has no cached challenge, and never clears it:
self._request_copy = None(async :84; sync :95)on_request: with no cached challenge and a request that has content,self._request_copy = request.http_request(async :179; sync :187), and a bodiless copy is sent to get the challengeon_challenge:if self._request_copy: request.http_request = self._request_copy(async :224-225; sync :232-233)
on_challenge runs on any later 401 that carries WWW-Authenticate, not only the first. So on a long-lived client, a 401 on an unrelated later request replaces that request with the client's very first request and re-sends it with a fresh token. For a certificate client, that is a second POST .../create for a certificate the caller did not ask about (a new certificate version), and the request the caller made is never retried.
To Reproduce (offline: a recording transport and a fake credential, no network)
"""Offline repro: azure-keyvault-certificates' async challenge policy re-sends an earlier request on a later 401.
No network: a recording transport answers from a script, and a fake credential returns a token.
"""
import asyncio
import time
from azure.core.credentials import AccessToken
from azure.core.pipeline.transport import AsyncHttpTransport
from azure.core.rest._http_response_impl_async import AsyncHttpResponseImpl
from azure.core.utils import CaseInsensitiveDict
from azure.keyvault.certificates import CertificatePolicy
from azure.keyvault.certificates.aio import CertificateClient
VAULT_URL = "https://example-vault.vault.azure.net"
CHALLENGE = 'Bearer authorization="https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000", resource="https://vault.azure.net"'
class _Internal:
async def close(self):
return None
class RecordingTransport(AsyncHttpTransport):
"""401 to any request without a bearer token; 401 once to a bearer GET when reject_next_get is set; else 200/202."""
def __init__(self):
self.seen = []
self.reject_next_get = False
async def open(self): return None
async def close(self): return None
async def __aenter__(self): return self
async def __aexit__(self, *exc): return None
async def send(self, request, **kwargs):
content = request.content
body = content.decode() if isinstance(content, bytes) else (content or "")
authorized = "Authorization" in request.headers
path = request.url[len(VAULT_URL):].split("?")[0]
self.seen.append((request.method, path, "body" if body else "no body", "auth" if authorized else "no auth"))
name = path.split("/")[2]
headers = CaseInsensitiveDict({"Content-Type": "application/json"})
status = 202 if request.method == "POST" else 200
payload = ('{"id": "%s/certificates/%s/pending", "status": "inProgress", "issuer": {"name": "Unknown"}, "request_id": "r"}' % (VAULT_URL, name)).encode()
if not authorized or (request.method == "GET" and self.reject_next_get):
if authorized:
self.reject_next_get = False
status, payload = 401, b'{"error": {"code": "Unauthorized"}}'
headers["WWW-Authenticate"] = CHALLENGE
response = AsyncHttpResponseImpl(request=request, internal_response=_Internal(), block_size=None, status_code=status,
reason="mock", content_type="application/json", headers=headers, stream_download_generator=None)
response._content = payload
return response
class FakeCredential:
async def get_token(self, *scopes, **kwargs):
return AccessToken("fake-token", int(time.time()) + 3600)
async def close(self):
return None
async def main():
transport = RecordingTransport()
async with CertificateClient(vault_url=VAULT_URL, credential=FakeCredential(), transport=transport) as client:
# 1. The client's first request carries a body: the challenge policy keeps a copy of it.
await client.create_certificate(certificate_name="cert-a", policy=CertificatePolicy(issuer_name="Self", subject="CN=cert-a"))
transport.seen.clear()
# 2. Later, an unrelated GET gets one 401 with a challenge (e.g. a token the service no longer accepts).
transport.reject_next_get = True
try:
await client.get_certificate_operation(certificate_name="cert-b")
outcome = "returned"
except Exception as exc: # noqa: BLE001
outcome = f"raised {type(exc).__name__}"
print("requests sent after the 401 on GET cert-b:")
for entry in transport.seen:
print(" ", *entry)
print("get_certificate_operation(cert-b):", outcome)
asyncio.run(main())
Output:
requests sent after the 401 on GET cert-b:
GET /certificates/cert-b/pending no body auth
POST /certificates/cert-a/create body auth
get_certificate_operation(cert-b): raised HttpResponseError
Expected behavior
After the 401 on GET /certificates/cert-b/pending, the policy re-sends that GET with a fresh token. The kept copy should be used only to complete the challenge round of the request it was taken from, and then cleared (or keyed to that request).
Additional context
It only needs a 401 with a WWW-Authenticate header after the client's first request carried a body, for example a token the service stops accepting while the client lives. A process-wide client (the documented pattern) is the case where this bites. We work around it locally by priming the challenge with a bodiless request first and refusing to send a request other than the one the caller made.
- 主要语言
- Python
- 星标
- 5.6k
- 派生
- 3.4k
- 平均合并
- 2 天 12 小时
- 30 天内合并 PR
- 204
环境准备
在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 阅读贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
Azure/azure-sdk-for-python 的其他 Issue
-
[Teams Chatbot]: next-mypy install 404s should point to CFS auth or --pypi workaround可能已有人在做 @jianingwang123 于 4 天前认领。 未关闭needs-triage
难度 1/5 1-3 小时 新手友好度 72/100
Azure/azure-sdk-for-python#49886 · 1 条评论 · 2 个 reaction · 已指派 2 人 ·
维护者通常 1 天内回复
-
[AI Projects] AIProjectInstrumentor: non-streamed responses.create raises AttributeError ('NonRecordingSpan' object has no attribute 'attributes') when the span is not sampled可能已有人在做 @14H034160212 于 6 天前认领。 未关闭AI Projects Client customer-reported question
难度 2/5 1-3 小时 新手友好度 78/100
Azure/azure-sdk-for-python#49854 · 2 条评论 · 1 个 reaction ·
维护者通常 1 天内回复
-
azure-eventhub migration guide: legacy checkpoint sample writes "None" metadata for partitions that were never checkpointed可能已有人在做 @hidingmyname 于 14 天前认领。 未关闭Client customer-reported Event Hubs needs-team-attention question Service Attention
难度 2/5 1-3 小时 新手友好度 78/100
Azure/azure-sdk-for-python#49723 · 2 条评论 · 1 个 reaction ·
维护者通常 1 天内回复
-
Scheduled test pipelines fail at the Analyze_Test stage due to date formatting可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭EngSys
难度 2/5 1-3 小时 新手友好度 78/100
Azure/azure-sdk-for-python#49614 · 1 条评论 · 1 个 reaction ·
维护者通常 1 天内回复
-
[Teams Chatbot]: Clarify Python dev-feed publication from a branch versus PyPI release可能已有人在做 @jianingwang123 于 29 天前认领。 未关闭customer-reported question
难度 2/5 1-3 小时 新手友好度 62/100
Azure/azure-sdk-for-python#49548 · 1 条评论 · 2 个 reaction · 已指派 2 人 ·
维护者通常 1 天内回复
查看 Azure/azure-sdk-for-python 的全部 Issue
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 72/100
维护者通常 1 天内回复
-
AI bug
难度 2/5 1-3 小时 新手友好度 68/100
AUTO-MAS-Project/AUTO-MAS#1960 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 82/100
btclib-org/btclib-wallet#872 ·
维护者通常 1 天内回复
-
[Bug]: Scheduled returns scan all accepted minion keys causing extreme load可能已有人在做 关联的 PR 仍在进行中或已合并。 未关闭bug needs-triage
难度 2/5 1-3 小时 新手友好度 76/100
saltstack/salt#70789 · 1 条评论 ·
维护者通常 3 天内回复
-
难度 2/5 1-3 小时 新手友好度 87/100
btclib-org/btclib#3093 ·
维护者通常 1 天内回复