PrefectHQ/fastmcp · error · TokenError
invalid_grant
invalid_grant
Error message
invalid_grant: Authorization code not found or already used.
What it means
When exchanging an authorization code for tokens, the provider checks its auth_codes dict for the submitted code. If it's absent — never issued, already consumed, or expired and purged — it raises TokenError('invalid_grant', 'Authorization code not found or already used.') per RFC 6749 §5.2.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/providers/in_memory.py:175
auth_code_obj = self.auth_codes.get(authorization_code)
if auth_code_obj:
if auth_code_obj.client_id != client.client_id:
return None # Belongs to a different client
if auth_code_obj.expires_at < time.time():
del self.auth_codes[authorization_code] # Expired
return None
return auth_code_obj
return None
async def exchange_authorization_code(
self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
) -> OAuthToken:
# Authorization code should have been validated (existence, expiry, client_id match)
# by the TokenHandler calling load_authorization_code before this.
# We might want to re-verify or simply trust it's valid.
if authorization_code.code not in self.auth_codes:
raise TokenError(
"invalid_grant", "Authorization code not found or already used."
)
# Consume the auth code
del self.auth_codes[authorization_code.code]
access_token_value = f"test_access_token_{secrets.token_hex(32)}"
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_SECONDS
)
View on GitHub (pinned to 1f02114297)
Solutions
- Use each authorization code exactly once; on retry-after-failure, restart the authorization flow to get a fresh code
- Keep the provider instance alive across the authorize/token round trip — state is in-memory only
- If replaying is legitimate in tests, re-run authorize() to mint a new code instead of reusing the old one
Example fix
// before await provider.exchange_authorization_code(client, code) # succeeds, code deleted await provider.exchange_authorization_code(client, code) # invalid_grant // after tokens = await provider.exchange_authorization_code(client, code) # call once, store tokens # new flow needed for another code:
Defensive patterns
Strategy: try-catch
Validate before calling
if authorization_code.code not in provider.auth_codes:
# code consumed or never issued — restart the authorization flow
authorization_code = await start_authorization_flow(client) Type guard
def code_is_fresh(provider, code) -> bool:
return code.code in provider.auth_codes Try / catch
try:
tokens = await provider.exchange_authorization_code(client, authorization_code)
except TokenError as e:
if e.error == "invalid_grant":
tokens = await restart_authorization_flow(client) # fresh code, single-use Prevention
- Never retry token exchange with the same code — treat codes as single-use
- Avoid process restarts between /authorize and /token with in-memory storage
- Serialize token exchange per code to avoid concurrent consumption races
When it happens
Trigger: Calling exchange_authorization_code twice with the same code (codes are deleted after first use, per single-use rule), or with a fabricated/unknown code, or after provider restart cleared in-memory auth_codes.
Common situations: HTTP client retries the token request after a network timeout, replaying a consumed code; two concurrent token requests racing on the same code; server process restarted between /authorize and /token (in-memory state lost).
Related errors
- invalid_grant
- The device authorization request expired
- The device authorization request was denied
- Device authorization failed
- OAuth client not found - cached credentials may be stale
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/cb3de528ff7027ab.
Report an issue: GitHub.