PrefectHQ/fastmcp · error · TokenError
invalid_scope
invalid_scope
Error message
invalid_scope: Requested scopes exceed those authorized by the refresh token.
What it means
During refresh-token exchange, the requested scopes for the new access token must be a subset of the scopes originally granted with the refresh token. Requesting anything beyond that raises TokenError('invalid_scope', ...) per RFC 6749 §6, which forbids privilege escalation through refresh.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/providers/in_memory.py:245
if token_obj.expires_at is not None and token_obj.expires_at < time.time():
self._revoke_internal(
refresh_token_str=token_obj.token
) # Clean up expired
return None
return token_obj
return None
async def exchange_refresh_token(
self,
client: OAuthClientInformationFull,
refresh_token: RefreshToken, # This is the RefreshToken object, already loaded
scopes: list[str], # Requested scopes for the new access token
) -> OAuthToken:
# Validate scopes: requested scopes must be a subset of original scopes
original_scopes = set(refresh_token.scopes)
requested_scopes = set(scopes)
if not requested_scopes.issubset(original_scopes):
raise TokenError(
"invalid_scope",
"Requested scopes exceed those authorized by the refresh token.",
)
# Invalidate old refresh token and its associated access token (rotation)
self._revoke_internal(refresh_token_str=refresh_token.token)
# Issue new tokens
new_access_token_value = f"test_access_token_{secrets.token_hex(32)}"
new_refresh_token_value = f"test_refresh_token_{secrets.token_hex(32)}"
access_token_expires_at = int(time.time() + DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS)
# Refresh token expiry
refresh_token_expires_at = None
if DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS is not None:
refresh_token_expires_at = int(
time.time() + DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDSView on GitHub (pinned to 1f02114297)
Solutions
- Pass an empty/None scopes list on refresh so the original grant's scopes are reused
- Request only scopes within the original grant: set(requested) <= set(original)
- If broader scopes are genuinely needed, run a fresh authorization flow to obtain a new grant and refresh token
Example fix
// before tokens = await provider.exchange_refresh_token(rt, client, ["read", "write", "admin"]) # rt only has read/write // after tokens = await provider.exchange_refresh_token(rt, client, []) # reuse original scopes # or request a new grant with 'admin' first
Defensive patterns
Strategy: validation
Validate before calling
requested = scopes or []
original = set(refresh_token.scopes)
assert set(requested).issubset(original), f"cannot escalate scopes: {set(requested) - original}"
tokens = await provider.exchange_refresh_token(refresh_token, client, requested) Type guard
def scopes_within_grant(refresh_token, scopes: list[str] | None) -> bool:
return set(scopes or []).issubset(set(refresh_token.scopes)) Try / catch
try:
tokens = await provider.exchange_refresh_token(rt, client, scopes)
except TokenError as e:
if e.error == "invalid_scope":
tokens = await provider.exchange_refresh_token(rt, client, []) # reuse original scopes Prevention
- Default to omitting scopes on refresh to inherit the original grant
- Store the granted scopes client-side and intersect before refreshing
- Run a fresh authorization flow whenever new scopes are genuinely needed
When it happens
Trigger: exchange_refresh_token(refresh_token, client, scopes) where set(scopes) contains any scope not present in refresh_token.scopes — e.g. asking for 'admin' when the original grant was only 'read write'.
Common situations: Client app hardcodes its full desired scope list in the refresh call instead of omitting scopes (omitting usually means reuse original); app was updated to need new scopes after the original consent; scope-name drift between grant and refresh requests.
Related errors
- Assertion missing required scopes: {sorted(missing)}
- AzureProvider requires at least one non-OIDC scope in requir
- Requested scopes are not valid: {', '.join(invalid_scopes)}
- Authorization failed for tool '{tool_name}': insufficient sc
- Authorization failed for resource '{uri}': insufficient scop
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/50ef3c4a7f2f28fb.
Report an issue: GitHub.