Skip to content

Commit a6fc0bc

Browse files
rohitsinghal4usinghalrohit4uCopilot
authored
Cache Key Vault challenges after validation (#48710)
* Cache Key Vault challenges after validation Move Key Vault challenge cache updates until after challenge resource verification succeeds so rejected challenges are not reused by later requests. Add regression coverage across the Key Vault packages to verify rejected challenges are not cached and do not authorize subsequent requests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Cache async Key Vault challenges after validation Apply the same post-validation challenge cache update to async Key Vault challenge authentication policies. Add async regression coverage to verify rejected challenges are not cached and do not authorize later requests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add CHANGELOG entries and restore async test blank-line formatting Addresses PR review feedback: add Unreleased 'Bugs Fixed' CHANGELOG entries for the challenge cache-validation fix across all five Key Vault packages, and restore PEP 8 blank-line separators between top-level tests in the async challenge-auth tests (blank lines only, no other reformatting). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Set Key Vault changelog release dates Set the affected Key Vault package changelog entries to the 2026-08-25 release date. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove empty CHANGELOG sections for Key Vault release entries The changelog verification (Verify ChangeLogEntries) fails when a dated release entry contains empty sections. Remove the empty Features Added, Breaking Changes, and Other Changes sections from the current release entry in each Key Vault package changelog, keeping the populated Bugs Fixed section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Rohit Singhal <singhalrohit@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a515035 commit a6fc0bc

24 files changed

Lines changed: 360 additions & 40 deletions

File tree

sdk/keyvault/azure-keyvault-administration/CHANGELOG.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
# Release History
22

3-
## 4.8.0b3 (Unreleased)
4-
5-
### Features Added
6-
7-
### Breaking Changes
3+
## 4.8.0b3 (2026-08-25)
84

95
### Bugs Fixed
106

11-
### Other Changes
7+
- Fixed a bug in the challenge authentication policy where the authentication challenge was cached before the challenge resource was verified. The challenge is now cached only after resource verification succeeds [#48710](https://github.com/Azure/azure-sdk-for-python/pull/48710).
128

139
## 4.8.0b2 (2026-07-08)
1410

sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/async_challenge_auth_policy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ async def on_challenge(self, request: PipelineRequest, response: PipelineRespons
215215
"See https://aka.ms/azsdk/blog/vault-uri for more information."
216216
)
217217

218+
ChallengeCache.set_challenge_for_url(request.http_request.url, challenge)
219+
218220
# If we stashed the original request in on_request, use it now to send along the original body content
219221
request_copy = request.context.get(_REQUEST_COPY_KEY)
220222
if request_copy:

sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/challenge_auth_policy.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def _has_claims(challenge: str) -> bool:
6464

6565

6666
def _update_challenge(request: PipelineRequest, challenger: PipelineResponse) -> HttpChallenge:
67-
"""Parse challenge from a challenge response, cache it, and return it.
67+
"""Parse challenge from a challenge response and return it.
6868
6969
:param request: The pipeline request that prompted the challenge response.
7070
:type request: ~azure.core.pipeline.PipelineRequest
@@ -80,7 +80,6 @@ def _update_challenge(request: PipelineRequest, challenger: PipelineResponse) ->
8080
challenger.http_response.headers.get("WWW-Authenticate"),
8181
response_headers=challenger.http_response.headers,
8282
)
83-
ChallengeCache.set_challenge_for_url(request.http_request.url, challenge)
8483
return challenge
8584

8685

@@ -234,6 +233,8 @@ def on_challenge(self, request: PipelineRequest, response: PipelineResponse) ->
234233
"See https://aka.ms/azsdk/blog/vault-uri for more information."
235234
)
236235

236+
ChallengeCache.set_challenge_for_url(request.http_request.url, challenge)
237+
237238
# If we stashed the original request in on_request, use it now to send along the original body content
238239
request_copy = request.context.get(_REQUEST_COPY_KEY)
239240
if request_copy:

sdk/keyvault/azure-keyvault-administration/tests/test_challenge_auth.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,71 @@ def get_random_url():
4747
return f"https://{uuid4()}.vault.azure.net/{uuid4()}".replace("-", "")
4848

4949

50+
@empty_challenge_cache
51+
def test_rejected_challenge_is_not_cached():
52+
url = "https://example.net/backup/canary"
53+
challenge = Mock(
54+
status_code=401,
55+
headers={"WWW-Authenticate": 'Bearer authorization="https://authority.net/tenant", resource=https://vault.azure.net'},
56+
)
57+
58+
class Requests:
59+
count = 0
60+
61+
def send(request):
62+
Requests.count += 1
63+
assert "Authorization" not in request.headers
64+
assert not request.body
65+
assert request.headers["Content-Length"] == "0"
66+
return challenge
67+
68+
credential = Mock(spec_set=["get_token"], get_token=Mock(side_effect=AssertionError("unexpected token request")))
69+
pipeline = Pipeline(policies=[ChallengeAuthPolicy(credential=credential)], transport=Mock(send=send))
70+
71+
for _ in range(2):
72+
request = HttpRequest("POST", url)
73+
request.set_bytes_body(b"secret")
74+
with pytest.raises(ValueError):
75+
pipeline.run(request)
76+
77+
assert Requests.count == 2
78+
assert not HttpChallengeCache.get_challenge_for_url(url)
79+
assert credential.get_token.call_count == 0
80+
81+
82+
@pytest.mark.asyncio
83+
@async_empty_challenge_cache
84+
async def test_rejected_challenge_is_not_cached_async():
85+
url = "https://example.net/backup/canary"
86+
challenge = Mock(
87+
status_code=401,
88+
headers={"WWW-Authenticate": 'Bearer authorization="https://authority.net/tenant", resource=https://vault.azure.net'},
89+
)
90+
91+
class Requests:
92+
count = 0
93+
94+
async def send(request):
95+
Requests.count += 1
96+
assert "Authorization" not in request.headers
97+
assert not request.body
98+
assert request.headers["Content-Length"] == "0"
99+
return challenge
100+
101+
credential = Mock(spec_set=["get_token"], get_token=Mock(side_effect=AssertionError("unexpected token request")))
102+
pipeline = AsyncPipeline(policies=[AsyncChallengeAuthPolicy(credential=credential)], transport=Mock(send=send))
103+
104+
for _ in range(2):
105+
request = HttpRequest("POST", url)
106+
request.set_bytes_body(b"secret")
107+
with pytest.raises(ValueError):
108+
await pipeline.run(request)
109+
110+
assert Requests.count == 2
111+
assert not HttpChallengeCache.get_challenge_for_url(url)
112+
assert credential.get_token.call_count == 0
113+
114+
50115
@empty_challenge_cache
51116
@pytest.mark.parametrize("token_type", TOKEN_TYPES)
52117
def test_request_body_not_reused_across_requests(token_type):

sdk/keyvault/azure-keyvault-certificates/CHANGELOG.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
# Release History
22

3-
## 4.12.0b3 (Unreleased)
4-
5-
### Features Added
6-
7-
### Breaking Changes
3+
## 4.12.0b3 (2026-08-25)
84

95
### Bugs Fixed
106

11-
### Other Changes
7+
- Fixed a bug in the challenge authentication policy where the authentication challenge was cached before the challenge resource was verified. The challenge is now cached only after resource verification succeeds [#48710](https://github.com/Azure/azure-sdk-for-python/pull/48710).
128

139
## 4.12.0b2 (2026-08-12)
1410

sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/async_challenge_auth_policy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,8 @@ async def on_challenge(self, request: PipelineRequest, response: PipelineRespons
219219
"See https://aka.ms/azsdk/blog/vault-uri for more information."
220220
)
221221

222+
ChallengeCache.set_challenge_for_url(request.http_request.url, challenge)
223+
222224
# If we stashed the original request in on_request, use it now to send along the original body content
223225
request_copy = request.context.get(_REQUEST_COPY_KEY)
224226
if request_copy:

sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/challenge_auth_policy.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def _has_claims(challenge: str) -> bool:
6464

6565

6666
def _update_challenge(request: PipelineRequest, challenger: PipelineResponse) -> HttpChallenge:
67-
"""Parse challenge from a challenge response, cache it, and return it.
67+
"""Parse challenge from a challenge response and return it.
6868
6969
:param request: The pipeline request that prompted the challenge response.
7070
:type request: ~azure.core.pipeline.PipelineRequest
@@ -80,7 +80,6 @@ def _update_challenge(request: PipelineRequest, challenger: PipelineResponse) ->
8080
challenger.http_response.headers.get("WWW-Authenticate"),
8181
response_headers=challenger.http_response.headers,
8282
)
83-
ChallengeCache.set_challenge_for_url(request.http_request.url, challenge)
8483
return challenge
8584

8685

@@ -234,6 +233,8 @@ def on_challenge(self, request: PipelineRequest, response: PipelineResponse) ->
234233
"See https://aka.ms/azsdk/blog/vault-uri for more information."
235234
)
236235

236+
ChallengeCache.set_challenge_for_url(request.http_request.url, challenge)
237+
237238
# If we stashed the original request in on_request, use it now to send along the original body content
238239
request_copy = request.context.get(_REQUEST_COPY_KEY)
239240
if request_copy:

sdk/keyvault/azure-keyvault-certificates/tests/test_challenge_auth.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,38 @@ def get_random_url():
3737
return f"https://{uuid4()}.vault.azure.net/{uuid4()}".replace("-", "")
3838

3939

40+
@empty_challenge_cache
41+
def test_rejected_challenge_is_not_cached():
42+
url = "https://example.net/certificates/canary"
43+
challenge = Mock(
44+
status_code=401,
45+
headers={"WWW-Authenticate": 'Bearer authorization="https://authority.net/tenant", resource=https://vault.azure.net'},
46+
)
47+
48+
class Requests:
49+
count = 0
50+
51+
def send(request):
52+
Requests.count += 1
53+
assert "Authorization" not in request.headers
54+
assert not request.body
55+
assert request.headers["Content-Length"] == "0"
56+
return challenge
57+
58+
credential = Mock(spec_set=["get_token"], get_token=Mock(side_effect=AssertionError("unexpected token request")))
59+
pipeline = Pipeline(policies=[ChallengeAuthPolicy(credential=credential)], transport=Mock(send=send))
60+
61+
for _ in range(2):
62+
request = HttpRequest("POST", url)
63+
request.set_bytes_body(b"secret")
64+
with pytest.raises(ValueError):
65+
pipeline.run(request)
66+
67+
assert Requests.count == 2
68+
assert not HttpChallengeCache.get_challenge_for_url(url)
69+
assert credential.get_token.call_count == 0
70+
71+
4072
@empty_challenge_cache
4173
@pytest.mark.parametrize("token_type", TOKEN_TYPES)
4274
def test_request_body_not_reused_across_requests(token_type):

sdk/keyvault/azure-keyvault-certificates/tests/test_challenge_auth_async.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,39 @@ async def wrapper(**kwargs):
3030
return wrapper
3131

3232

33+
@pytest.mark.asyncio
34+
@empty_challenge_cache
35+
async def test_rejected_challenge_is_not_cached():
36+
url = "https://example.net/certificates/canary"
37+
challenge = Mock(
38+
status_code=401,
39+
headers={"WWW-Authenticate": 'Bearer authorization="https://authority.net/tenant", resource=https://vault.azure.net'},
40+
)
41+
42+
class Requests:
43+
count = 0
44+
45+
async def send(request):
46+
Requests.count += 1
47+
assert "Authorization" not in request.headers
48+
assert not request.body
49+
assert request.headers["Content-Length"] == "0"
50+
return challenge
51+
52+
credential = Mock(spec_set=["get_token"], get_token=Mock(side_effect=AssertionError("unexpected token request")))
53+
pipeline = AsyncPipeline(policies=[AsyncChallengeAuthPolicy(credential=credential)], transport=Mock(send=send))
54+
55+
for _ in range(2):
56+
request = HttpRequest("POST", url)
57+
request.set_bytes_body(b"secret")
58+
with pytest.raises(ValueError):
59+
await pipeline.run(request)
60+
61+
assert Requests.count == 2
62+
assert not HttpChallengeCache.get_challenge_for_url(url)
63+
assert credential.get_token.call_count == 0
64+
65+
3366
@pytest.mark.asyncio
3467
@empty_challenge_cache
3568
@pytest.mark.parametrize("token_type", TOKEN_TYPES)

sdk/keyvault/azure-keyvault-keys/CHANGELOG.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
# Release History
22

3-
## 4.12.0b4 (Unreleased)
4-
5-
### Features Added
6-
7-
### Breaking Changes
3+
## 4.12.0b4 (2026-08-25)
84

95
### Bugs Fixed
106

11-
### Other Changes
7+
- Fixed a bug in the challenge authentication policy where the authentication challenge was cached before the challenge resource was verified. The challenge is now cached only after resource verification succeeds [#48710](https://github.com/Azure/azure-sdk-for-python/pull/48710).
128

139
## 4.12.0b3 (2026-07-08)
1410

0 commit comments

Comments
 (0)