@@ -53,13 +53,13 @@ def __init__(self):
5353 async def get_tokens (self ) -> OAuthToken | None :
5454 return self ._tokens
5555
56- async def set_tokens (self , tokens : OAuthToken ) -> None :
56+ async def set_tokens (self , tokens : OAuthToken | None ) -> None :
5757 self ._tokens = tokens
5858
5959 async def get_client_info (self ) -> OAuthClientInformationFull | None :
6060 return self ._client_info
6161
62- async def set_client_info (self , client_info : OAuthClientInformationFull ) -> None :
62+ async def set_client_info (self , client_info : OAuthClientInformationFull | None ) -> None :
6363 self ._client_info = client_info
6464
6565
@@ -264,6 +264,26 @@ def test_clear_tokens(self, oauth_provider: OAuthClientProvider, valid_tokens: O
264264 assert context .current_tokens is None
265265 assert context .token_expiry_time is None
266266
267+ @pytest .mark .anyio
268+ async def test_initialize_discards_expired_client_registration (
269+ self , oauth_provider : OAuthClientProvider , mock_storage : MockTokenStorage , valid_tokens : OAuthToken
270+ ):
271+ expired_client = OAuthClientInformationFull (
272+ client_id = "expired-client" ,
273+ client_secret = "expired-secret" ,
274+ client_secret_expires_at = int (time .time ()) - 1 ,
275+ redirect_uris = [AnyUrl ("http://localhost:3030/callback" )],
276+ )
277+ await mock_storage .set_client_info (expired_client )
278+ await mock_storage .set_tokens (valid_tokens )
279+
280+ await oauth_provider ._initialize ()
281+
282+ assert oauth_provider .context .client_info is None
283+ assert oauth_provider .context .current_tokens is None
284+ assert await mock_storage .get_client_info () is None
285+ assert await mock_storage .get_tokens () is None
286+
267287
268288class TestOAuthFlow :
269289 """Test OAuth flow methods."""
@@ -2955,6 +2975,34 @@ async def test_handle_token_response_raises_on_non_2xx_with_body(oauth_provider:
29552975 await oauth_provider ._handle_token_response (response )
29562976
29572977
2978+ @pytest .mark .anyio
2979+ async def test_handle_token_response_invalid_client_clears_stored_credentials (
2980+ oauth_provider : OAuthClientProvider , mock_storage : MockTokenStorage , valid_tokens : OAuthToken
2981+ ):
2982+ client_info = OAuthClientInformationFull (
2983+ client_id = "stale-client" ,
2984+ client_secret = "stale-secret" ,
2985+ redirect_uris = [AnyUrl ("http://localhost:3030/callback" )],
2986+ )
2987+ oauth_provider .context .client_info = client_info
2988+ oauth_provider .context .current_tokens = valid_tokens
2989+ await mock_storage .set_client_info (client_info )
2990+ await mock_storage .set_tokens (valid_tokens )
2991+ response = httpx2 .Response (
2992+ 401 ,
2993+ json = {"error" : "invalid_client" },
2994+ request = httpx2 .Request ("POST" , "https://auth.example.com/token" ),
2995+ )
2996+
2997+ with pytest .raises (OAuthTokenError , match = r"Token exchange failed \(401\).*invalid_client" ):
2998+ await oauth_provider ._handle_token_response (response )
2999+
3000+ assert oauth_provider .context .client_info is None
3001+ assert oauth_provider .context .current_tokens is None
3002+ assert await mock_storage .get_client_info () is None
3003+ assert await mock_storage .get_tokens () is None
3004+
3005+
29583006@pytest .mark .anyio
29593007async def test_handle_refresh_response_carries_prior_scope_and_refresh_token_when_omitted (
29603008 oauth_provider : OAuthClientProvider , mock_storage : MockTokenStorage
@@ -3007,6 +3055,34 @@ async def test_handle_refresh_response_adopts_rotated_refresh_token_when_returne
30073055 assert stored .refresh_token == "rotated"
30083056
30093057
3058+ @pytest .mark .anyio
3059+ async def test_handle_refresh_response_invalid_client_clears_stored_credentials (
3060+ oauth_provider : OAuthClientProvider , mock_storage : MockTokenStorage , valid_tokens : OAuthToken
3061+ ):
3062+ client_info = OAuthClientInformationFull (
3063+ client_id = "stale-client" ,
3064+ client_secret = "stale-secret" ,
3065+ redirect_uris = [AnyUrl ("http://localhost:3030/callback" )],
3066+ )
3067+ oauth_provider .context .client_info = client_info
3068+ oauth_provider .context .current_tokens = valid_tokens
3069+ await mock_storage .set_client_info (client_info )
3070+ await mock_storage .set_tokens (valid_tokens )
3071+ response = httpx2 .Response (
3072+ 400 ,
3073+ json = {"error" : "invalid_client" },
3074+ request = httpx2 .Request ("POST" , "https://auth.example.com/token" ),
3075+ )
3076+
3077+ ok = await oauth_provider ._handle_refresh_response (response )
3078+
3079+ assert ok is False
3080+ assert oauth_provider .context .client_info is None
3081+ assert oauth_provider .context .current_tokens is None
3082+ assert await mock_storage .get_client_info () is None
3083+ assert await mock_storage .get_tokens () is None
3084+
3085+
30103086@pytest .mark .anyio
30113087async def test_issuer_binding_re_evaluated_after_asm_when_prm_discovery_failed (
30123088 oauth_provider : OAuthClientProvider ,
0 commit comments