From 63888dfa5cdfc2c4ca851339b820691b51672309 Mon Sep 17 00:00:00 2001 From: pei711 <199601925+pei711@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:07:41 +0800 Subject: [PATCH] fix(auth): treat tokens as expired at exact expiration timestamp --- .../auth/extensions/identity_assertion.py | 2 +- src/mcp/client/auth/oauth2.py | 2 +- tests/client/test_auth.py | 20 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/mcp/client/auth/extensions/identity_assertion.py b/src/mcp/client/auth/extensions/identity_assertion.py index 5e715cb83a..4a659a35c9 100644 --- a/src/mcp/client/auth/extensions/identity_assertion.py +++ b/src/mcp/client/auth/extensions/identity_assertion.py @@ -167,7 +167,7 @@ async def _auth_flow(self, request: httpx2.Request) -> AsyncGenerator[httpx2.Req self._expiry = calculate_token_expiry(self._tokens.expires_in) if self._tokens else None self._initialized = True - if self._tokens and (self._expiry is None or time.time() <= self._expiry): + if self._tokens and (self._expiry is None or time.time() < self._expiry): request.headers["Authorization"] = f"Bearer {self._tokens.access_token}" response = yield request diff --git a/src/mcp/client/auth/oauth2.py b/src/mcp/client/auth/oauth2.py index c513a99b46..f94a2f90c6 100644 --- a/src/mcp/client/auth/oauth2.py +++ b/src/mcp/client/auth/oauth2.py @@ -188,7 +188,7 @@ def is_token_valid(self) -> bool: return bool( self.current_tokens and self.current_tokens.access_token - and (not self.token_expiry_time or time.time() <= self.token_expiry_time) + and (not self.token_expiry_time or time.time() < self.token_expiry_time) ) def can_refresh_token(self) -> bool: diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index 18a1566705..9318ba5d5b 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -244,6 +244,26 @@ async def test_token_validity_checking(self, oauth_provider: OAuthClientProvider assert not context.is_token_valid() assert context.can_refresh_token() # Can still refresh + # Exactly at expiry boundary (current time equals expiry) + now = time.time() + context.token_expiry_time = now + with mock.patch("time.time", return_value=now): + assert not context.is_token_valid() # Token must not be accepted on or after expiry + + # Token with expires_in=0 should be immediately invalid at issuance time + zero_expiry_token = OAuthToken( + access_token="zero_token", + token_type="Bearer", + expires_in=0, + ) + context.current_tokens = zero_expiry_token + with mock.patch("time.time", return_value=now): + context.update_token_expiry(zero_expiry_token) + assert not context.is_token_valid() + + context.current_tokens = valid_tokens + context.token_expiry_time = time.time() - 100 + # Remove refresh token context.current_tokens.refresh_token = None assert not context.can_refresh_token()