You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There's a potential race condition in the OpenID configuration refresh mechanism. When multiple concurrent requests try to refresh the configuration simultaneously, all requests trigger separate calls to the configuration and keys endpoints, even though only one refresh is necessary.
To Reproduce
Create a scenario with expired or uninitialized OpenID configuration
Make multiple concurrent requests that would trigger a configuration refresh
Observe multiple identical calls to the configuration and keys endpoints
Example test case:
asyncdeftest_concurrent_refresh_requests():
"""Test that concurrent refreshes are handled correctly"""withrespx.mock(assert_all_called=True) asmock:
asyncdefslow_config_response(*args, **kwargs):
awaitasyncio.sleep(0.2)
returnhttpx.Response(200, json=openid_configuration())
asyncdefslow_keys_response(*args, **kwargs):
awaitasyncio.sleep(0.2)
returnhttpx.Response(200, json=build_openid_keys())
config_route=mock.get(openid_config_url()).mock(side_effect=slow_config_response)
keys_route=mock.get(keys_url()).mock(side_effect=slow_keys_response)
azure_scheme.openid_config._config_timestamp=Nonetasks= [azure_scheme.openid_config.load_config() for_inrange(5)]
awaitasyncio.gather(*tasks)
assertlen(config_route.calls) ==1, "Config endpoint called multiple times"assertlen(keys_route.calls) ==1, "Keys endpoint called multiple times"assertlen(azure_scheme.openid_config.signing_keys) ==2
Leading to:
FAILED tests/test_provider_config.py::test_concurrent_refresh_requests - AssertionError: Config endpoint called multiple times
Your configuration
n/a
Root cause analysis
The current implementation doesn't protect against concurrent refresh requests. When the configuration is expired or uninitialized and multiple requests arrive simultaneously, each request independently determines a refresh is needed and initiates its own HTTP calls.
Suggested fix
Use an asyncio.Lock() to ensure only one refresh operation happens at a time:
importasyncioclassOpenIdConfig:
def__init__(
self,
...
) ->None:
...
self._lock=asyncio.Lock() # add this to the global config objectasyncdefload_config(self) ->None:
""" Loads config from the Intility openid-config endpoint if it's over 24 hours old (or don't exist) """asyncwithself._lock: # acquire lock and refresh# do the refresh
This implementation would lead the above test to pass.
Now I'm wondering if such a race condition is actually a problem or is it just theoretical? I must admit I am no expert in asyncio but stumbled upon this issue when testing.
The text was updated successfully, but these errors were encountered:
Good catch! If no requests has been done to the endpoint in 24 hours, we refresh. This can be done 5 times, async, where each request will override the previous, as your test proves. Since this is something that very, very rarely changes, it won't have a security impact.
With that said, I am very much in favor of adding a lock. It makes it easier to reason about the code, and any external call we can avoid is good. If there are 5000 requests hitting this endpoint at the same time, the current implementation would struggle.
Race Condition in OpenID Configuration Refresh
Describe the bug
There's a potential race condition in the OpenID configuration refresh mechanism. When multiple concurrent requests try to refresh the configuration simultaneously, all requests trigger separate calls to the configuration and keys endpoints, even though only one refresh is necessary.
To Reproduce
Example test case:
Leading to:
Your configuration
n/a
Root cause analysis
The current implementation doesn't protect against concurrent refresh requests. When the configuration is expired or uninitialized and multiple requests arrive simultaneously, each request independently determines a refresh is needed and initiates its own HTTP calls.
Suggested fix
Use an
asyncio.Lock()
to ensure only one refresh operation happens at a time:This implementation would lead the above test to pass.
Now I'm wondering if such a race condition is actually a problem or is it just theoretical? I must admit I am no expert in asyncio but stumbled upon this issue when testing.
The text was updated successfully, but these errors were encountered: