Skip to content

Keep ROPC password hashes out of the persisted http_cache - #977

Open
rumaisaaaaa263 wants to merge 1 commit into
AzureAD:devfrom
rumaisaaaaa263:ropc-http-cache-no-password-key
Open

rumaisaaaaa263 wants to merge 1 commit into
AzureAD:devfrom
rumaisaaaaa263:ropc-http-cache-no-password-key

Conversation

@rumaisaaaaa263

Copy link
Copy Markdown

ThrottledHttpClient caches HTTP 400 token responses for 60 seconds under POST <token_endpoint> hash=<sha256> 400, where the hash is an unsalted SHA-256 over str(params) + str(data). For acquire_token_by_username_password() on a public client, data is client_id, grant_type, client_info, username, password and scope, so the password is the only input an attacker does not already know. ROPC gets a 400 with the correct password whenever the user needs MFA or consent (AADSTS50076, AADSTS65001). The http_cache docstring says its content holds no tokens or PII and needs no encryption, so apps persist it as-is: the docstring recipe pickles it next to the script, and Azure CLI pickles it to ~/.azure/msal_http_cache.bin on every write and uses this API for az login -u/-p. Anyone who can read that file can recover the password with an offline dictionary attack at SHA-256 speed. On dev, a scratch script that calls the public API against a fake token endpoint returning AADSTS50076, pickles the http_cache, and then hashes candidate passwords against the stored key recovers the password; with this change the cache stays empty for the password grant.

The 400 cache now skips the password grant the same way it already skips device flow. Hashing the body without the password would also close the leak, but a retry with a corrected password would then get the cached failure for 60 seconds. 429/5xx/Retry-After throttling for ROPC is unchanged because that key only hashes the username, and federated ROPC is not affected because its WS-Trust body carries a random MessageID. test_ropc_error_should_not_be_cached fails before the change and passes after, and the unit test command from .Pipelines/template-pipeline-stages.yml passes on Python 3.9 (456 passed, 20 skipped).

ThrottledHttpClient keys cached HTTP 400 token responses by an
unsalted SHA-256 of the request body. For the password grant, the
password is the only input in that body an attacker does not already
know, so a persisted http_cache allows an offline dictionary attack
on it. ROPC returns 400 with the correct password whenever MFA or
consent is required.

Skip the 400 cache for the password grant, as is already done for
device flow. 429/5xx/Retry-After throttling for ROPC is unchanged.
Copilot AI balanced review requested due to automatic review settings October 28, 2026 15:38
@rumaisaaaaa263
rumaisaaaaa263 requested a review from a team as a code owner October 28, 2026 15:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The review identifies no active issues with the change excluding password grants from HTTP 400 response caching.

No open issues 🎉

What changed in this PR

This PR prevents password-derived hashes from being persisted in MSAL’s HTTP cache by excluding resource owner password credentials (ROPC) grants from HTTP 400 response caching.

Changes:

  • Skips HTTP 400 caching for password grants while leaving other throttling behavior unchanged.
  • Adds a regression test verifying that repeated ROPC failures return fresh responses and leave the cache empty.
File Description
tests/​test_throttled_http_client.py Adds regression coverage for uncached ROPC failures.
msal/​throttled_http_client.py Excludes password grants from HTTP 400 response caching.

Review effort: Balanced

@rumaisaaaaa263

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree company="Digiscrypt Technologies"

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants