-
Notifications
You must be signed in to change notification settings - Fork 3.2k
[Key Vault] Make challenge caching case insensitive #44127
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR addresses a bug where Key Vault's challenge cache fails to retrieve cached challenges when URLs differ only in case (e.g., when Managed HSM LRO status monitor URLs are returned in lowercase). The fix makes the challenge cache case-insensitive by lower-casing all netloc values during cache operations.
Key Changes:
- Modified challenge cache to lower-case netloc values in all cache operations (get, set, remove, validate)
- Applied the fix consistently across all five Key Vault packages
- Added regression test to verify case-insensitive behavior
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/http_challenge_cache.py | Added .lower() calls to make cache operations case-insensitive |
| sdk/keyvault/azure-keyvault-secrets/azure/keyvault/secrets/_shared/http_challenge_cache.py | Added .lower() calls to make cache operations case-insensitive |
| sdk/keyvault/azure-keyvault-certificates/azure/keyvault/certificates/_shared/http_challenge_cache.py | Added .lower() calls to make cache operations case-insensitive |
| sdk/keyvault/azure-keyvault-administration/azure/keyvault/administration/_internal/http_challenge_cache.py | Added .lower() calls to make cache operations case-insensitive |
| sdk/keyvault/azure-keyvault-securitydomain/azure/keyvault/securitydomain/_internal/http_challenge_cache.py | Added .lower() calls to make cache operations case-insensitive |
| sdk/keyvault/azure-keyvault-keys/tests/test_challenge_auth.py | Added test_challenge_cache_casing to verify case-insensitive cache behavior |
| parsed = parse.urlparse(url) | ||
|
|
||
| with _lock: | ||
| del _cache[parsed.netloc] | ||
| del _cache[parsed.netloc.lower()] |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The remove_challenge_for_url function directly uses parsed.netloc.lower() as the cache key, but get_challenge_for_url uses _get_cache_key(url).lower() which has special logic to strip the default HTTPS port (:443). This inconsistency means that if a URL contains :443, the challenge can be set and retrieved, but cannot be removed correctly.
The fix should use _get_cache_key(url).lower() instead of parsed.netloc.lower():
key = _get_cache_key(url)
with _lock:
del _cache[key.lower()]| with _lock: | ||
| _cache[src_url.netloc] = challenge | ||
| _cache[src_url.netloc.lower()] = challenge |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The set_challenge_for_url function directly uses src_url.netloc.lower() as the cache key, but get_challenge_for_url uses _get_cache_key(url).lower() which has special logic to strip the default HTTPS port (:443). This inconsistency means that if a URL contains :443, the challenge would be stored with the port included but retrieved with the port stripped, causing cache misses.
The fix should use _get_cache_key(url).lower() instead of src_url.netloc.lower():
key = _get_cache_key(url)
with _lock:
_cache[key.lower()] = challengeNote: The validation on line 82 should remain as-is since it's comparing the source URL's authority with the challenge's source authority, which is separate from the cache key logic.
|
|
||
| with _lock: | ||
| _cache[src_url.netloc] = challenge | ||
| _cache[src_url.netloc.lower()] = challenge |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The set_challenge_for_url function directly uses src_url.netloc.lower() as the cache key, but get_challenge_for_url uses _get_cache_key(url).lower() which has special logic to strip the default HTTPS port (:443). This inconsistency means that if a URL contains :443, the challenge would be stored with the port included but retrieved with the port stripped, causing cache misses.
The fix should use _get_cache_key(url).lower() instead of src_url.netloc.lower():
key = _get_cache_key(url)
with _lock:
_cache[key.lower()] = challengeNote: The validation on line 82 should remain as-is since it's comparing the source URL's authority with the challenge's source authority, which is separate from the cache key logic.
| _cache[src_url.netloc.lower()] = challenge | |
| key = _get_cache_key(url) | |
| _cache[key.lower()] = challenge |
| parsed = parse.urlparse(url) | ||
|
|
||
| with _lock: | ||
| del _cache[parsed.netloc] | ||
| del _cache[parsed.netloc.lower()] |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The remove_challenge_for_url function directly uses parsed.netloc.lower() as the cache key, but get_challenge_for_url uses _get_cache_key(url).lower() which has special logic to strip the default HTTPS port (:443). This inconsistency means that if a URL contains :443, the challenge can be set and retrieved, but cannot be removed correctly.
The fix should use _get_cache_key(url).lower() instead of parsed.netloc.lower():
key = _get_cache_key(url)
with _lock:
del _cache[key.lower()]| parsed = parse.urlparse(url) | ||
|
|
||
| with _lock: | ||
| del _cache[parsed.netloc] | ||
| del _cache[parsed.netloc.lower()] |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The remove_challenge_for_url function directly uses parsed.netloc.lower() as the cache key, but get_challenge_for_url uses _get_cache_key(url).lower() which has special logic to strip the default HTTPS port (:443). This inconsistency means that if a URL contains :443, the challenge can be set and retrieved, but cannot be removed correctly.
The fix should use _get_cache_key(url).lower() instead of parsed.netloc.lower():
key = _get_cache_key(url)
with _lock:
del _cache[key.lower()]|
|
||
| with _lock: | ||
| _cache[src_url.netloc] = challenge | ||
| _cache[src_url.netloc.lower()] = challenge |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The set_challenge_for_url function directly uses src_url.netloc.lower() as the cache key, but get_challenge_for_url uses _get_cache_key(url).lower() which has special logic to strip the default HTTPS port (:443). This inconsistency means that if a URL contains :443, the challenge would be stored with the port included but retrieved with the port stripped, causing cache misses.
The fix should use _get_cache_key(url).lower() instead of src_url.netloc.lower():
key = _get_cache_key(url)
with _lock:
_cache[key.lower()] = challengeNote: The validation on line 82 should remain as-is since it's comparing the source URL's authority with the challenge's source authority, which is separate from the cache key logic.
| _cache[src_url.netloc.lower()] = challenge | |
| _cache[_get_cache_key(url).lower()] = challenge |
| parsed = parse.urlparse(url) | ||
|
|
||
| with _lock: | ||
| del _cache[parsed.netloc] | ||
| del _cache[parsed.netloc.lower()] |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The remove_challenge_for_url function directly uses parsed.netloc.lower() as the cache key, but get_challenge_for_url uses _get_cache_key(url).lower() which has special logic to strip the default HTTPS port (:443). This inconsistency means that if a URL contains :443, the challenge can be set and retrieved, but cannot be removed correctly.
The fix should use _get_cache_key(url).lower() instead of parsed.netloc.lower():
key = _get_cache_key(url)
with _lock:
del _cache[key.lower()]|
|
||
| with _lock: | ||
| _cache[src_url.netloc] = challenge | ||
| _cache[src_url.netloc.lower()] = challenge |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The set_challenge_for_url function directly uses src_url.netloc.lower() as the cache key, but get_challenge_for_url uses _get_cache_key(url).lower() which has special logic to strip the default HTTPS port (:443). This inconsistency means that if a URL contains :443, the challenge would be stored with the port included but retrieved with the port stripped, causing cache misses.
The fix should use _get_cache_key(url).lower() instead of src_url.netloc.lower():
key = _get_cache_key(url)
with _lock:
_cache[key.lower()] = challengeNote: The validation on line 82 should remain as-is since it's comparing the source URL's authority with the challenge's source authority, which is separate from the cache key logic.
| _cache[src_url.netloc.lower()] = challenge | |
| key = _get_cache_key(url) | |
| _cache[key.lower()] = challenge |
| parsed = parse.urlparse(url) | ||
|
|
||
| with _lock: | ||
| del _cache[parsed.netloc] | ||
| del _cache[parsed.netloc.lower()] |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The remove_challenge_for_url function directly uses parsed.netloc.lower() as the cache key, but get_challenge_for_url uses _get_cache_key(url).lower() which has special logic to strip the default HTTPS port (:443). This inconsistency means that if a URL contains :443, the challenge can be set and retrieved, but cannot be removed correctly.
The fix should use _get_cache_key(url).lower() instead of parsed.netloc.lower():
key = _get_cache_key(url)
with _lock:
del _cache[key.lower()]|
|
||
| with _lock: | ||
| _cache[src_url.netloc] = challenge | ||
| _cache[src_url.netloc.lower()] = challenge |
Copilot
AI
Nov 21, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The set_challenge_for_url function directly uses src_url.netloc.lower() as the cache key, but get_challenge_for_url uses _get_cache_key(url).lower() which has special logic to strip the default HTTPS port (:443). This inconsistency means that if a URL contains :443, the challenge would be stored with the port included but retrieved with the port stripped, causing cache misses.
The fix should use _get_cache_key(url).lower() instead of src_url.netloc.lower():
key = _get_cache_key(url)
with _lock:
_cache[key.lower()] = challengeNote: The validation on line 82 should remain as-is since it's comparing the source URL's authority with the challenge's source authority, which is separate from the cache key logic.
| _cache[src_url.netloc.lower()] = challenge | |
| _cache[_get_cache_key(url).lower()] = challenge |
Description
@Cherrett found an issue that currently only affects Managed HSM LROs, where the status monitor URL is lowercase even if the original request URL was not. The challenge cache is currently case sensitive, so the challenge flow is erroneously followed upon the first status monitor endpoint request.
This PR updates our challenge cache logic to always lower-case endpoints during operations and comparisons and tests this logic with a regression test.
All SDK Contribution checklist:
General Guidelines and Best Practices
Testing Guidelines