What happened?
⚠️ Disclaimer: This issue was identified through a combination of AI-assisted code tracing and manual research. While I've traced through the relevant code paths in exchange_authorization_code and exchange_refresh_token and believe the analysis to be accurate, I may have missed edge cases or misread parts of the flow. I'd encourage a maintainer to verify the analysis before acting on it. Happy to discuss or provide more context.
Summary
OAuthProxy has no way to configure a fallback refresh token expiry. The code hardcodes 30 days in multiple places, which silently overrides the actual expiry configured in the upstream provider. This mirrors the existing fallback_access_token_expiry_seconds parameter which correctly handles the same problem for access tokens.
Bug 1: exchange_authorization_code hardcodes 30-day JTI mapping TTL
https://github.com/PrefectHQ/fastmcp/blob/v3.2.4/src/fastmcp/server/auth/oauth_proxy/proxy.py#L1081-L1089
The JTI mapping for the refresh token is hardcoded to 30 days regardless of the actual refresh token expiry.
This is inconsistent - _refresh_token_store and _upstream_token_store are stored with ttl=refresh_expires_in, but the JTI mapping that links them together expires early.
Bug 2: exchange_refresh_token ignores refresh_expires_in when issuing the new FastMCP refresh JWT
When Cognito (and other providers) don't return a refresh_token in the refresh response, new_refresh_expires_in stays None, causing the new FastMCP refresh token JWT to be issued with a hardcoded 30-day expiry.
# exchange_refresh_token()
new_refresh_expires_in = None
if new_upstream_refresh := token_response.get("refresh_token"):
# Cognito does NOT return refresh_token on refresh → block skipped
# new_refresh_expires_in stays None
...
new_fastmcp_refresh = self.jwt_issuer.issue_refresh_token(
...
expires_in=new_refresh_expires_in or 60 * 60 * 24 * 30, # ❌ falls back to 30 days
)
Even though upstream_token_set.refresh_token_expires_at correctly holds the real expiry and refresh_ttl is calculated correctly, it is never used when issuing the JWT. So the store has the correct TTL but the JWT itself bakes in a 30-day exp claim — and since verify_token() checks exp first, users are forced to re-login after 30 days regardless of what's in client storage.
Proposed Fix
Add a fallback_refresh_token_expiry_seconds parameter to OAuthProxy mirroring the existing fallback_access_token_expiry_seconds.
Example Code
Version Information
FastMCP version: 3.2.0
MCP version: 1.27.0
Python version: 3.14.3
Platform: macOS-26.4-arm64-arm-64bit-Mach-O
What happened?
Summary
OAuthProxyhas no way to configure a fallback refresh token expiry. The code hardcodes 30 days in multiple places, which silently overrides the actual expiry configured in the upstream provider. This mirrors the existingfallback_access_token_expiry_secondsparameter which correctly handles the same problem for access tokens.Bug 1:
exchange_authorization_codehardcodes 30-day JTI mapping TTLhttps://github.com/PrefectHQ/fastmcp/blob/v3.2.4/src/fastmcp/server/auth/oauth_proxy/proxy.py#L1081-L1089
The JTI mapping for the refresh token is hardcoded to 30 days regardless of the actual refresh token expiry.
This is inconsistent -
_refresh_token_storeand_upstream_token_storeare stored withttl=refresh_expires_in, but the JTI mapping that links them together expires early.Bug 2:
exchange_refresh_tokenignoresrefresh_expires_inwhen issuing the new FastMCP refresh JWTWhen Cognito (and other providers) don't return a refresh_token in the refresh response,
new_refresh_expires_instays None, causing the new FastMCP refresh token JWT to be issued with a hardcoded 30-day expiry.Even though
upstream_token_set.refresh_token_expires_atcorrectly holds the real expiry andrefresh_ttlis calculated correctly, it is never used when issuing the JWT. So the store has the correct TTL but the JWT itself bakes in a 30-day exp claim — and sinceverify_token()checks exp first, users are forced to re-login after 30 days regardless of what's in client storage.Proposed Fix
Add a
fallback_refresh_token_expiry_secondsparameter toOAuthProxymirroring the existingfallback_access_token_expiry_seconds.Example Code
Version Information