{"record":{"id":"8863edc20bc5703b","repo":"PrefectHQ/fastmcp","slug":"invalid-grant","errorCode":"invalid_grant","errorMessage":"Authorization code not found","messagePattern":"Authorization code not found","errorType":"error_code","errorClass":"TokenError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py","lineNumber":1311,"sourceCode":"        \"\"\"Exchange authorization code for FastMCP-issued tokens.\n\n        Implements the token factory pattern:\n        1. Retrieves upstream tokens from stored authorization code\n        2. Extracts user identity from upstream token\n        3. Encrypts and stores upstream tokens\n        4. Issues FastMCP-signed JWT tokens\n        5. Returns FastMCP tokens (NOT upstream tokens)\n\n        PKCE validation is handled by the MCP framework before this method is called.\n        \"\"\"\n        # Look up stored code data\n        code_model = await self._code_store.get(key=authorization_code.code)\n        if not code_model:\n            logger.error(\n                \"Authorization code not found in client codes: %s\",\n                authorization_code.code,\n            )\n            raise TokenError(\"invalid_grant\", \"Authorization code not found\")\n\n        # Get stored upstream tokens\n        idp_tokens = code_model.idp_tokens\n\n        # Use IdP-granted scopes when available (RFC 6749 §5.1: the IdP MUST\n        # include a scope parameter when the granted scope differs from the\n        # requested scope).  Fall back to requested scopes only when the IdP\n        # omits scope, meaning it granted exactly what was requested.\n        granted_scopes: list[str] = (\n            parse_scopes(idp_tokens[\"scope\"]) or []\n            if \"scope\" in idp_tokens\n            else list(authorization_code.scopes)\n        )\n        # Translate IdP-wire scopes into the client-facing form before they\n        # propagate to storage, the FastMCP JWT, and the response body. Default\n        # implementation is identity; AzureProvider overrides this to strip the\n        # identifier_uri prefix Azure echoes back on custom API scopes.\n        granted_scopes = self._translate_scopes_from_idp(granted_scopes)","sourceCodeStart":1293,"sourceCodeEnd":1329,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py#L1293-L1329","documentation":"Raised during the OAuth proxy token exchange when the authorization code presented by the client is not found in the proxy's code store. The proxy stores authorization codes it issued and looks them up by key when the client redeems the code at /token; a miss means the code was never issued by this server, already consumed (codes are single-use per RFC 6749), or expired/purged from storage. The client receives an OAuth TokenError with code 'invalid_grant'.","triggerScenarios":"Calling POST /token with grant_type=authorization_code and a code that is not in the proxy's _code_store: a reused (already-redeemed) code, a code from a different server/environment, an expired or evicted entry (e.g. in-memory store restarted), or a tampered code value.","commonSituations":"Retrying a token exchange after an earlier success (code was single-use and deleted); client caches an auth code across redirects; server restart with an in-memory code store losing issued codes; load-balanced instances sharing no storage; clock skew or storage TTL expiring the code before redemption.","solutions":["Do not reuse authorization codes — restart the OAuth flow and obtain a fresh code from the authorization endpoint.","If codes must survive restarts or multiple replicas, configure a persistent/shared storage backend for the code store instead of the default in-memory store.","Check that the authorization and token requests hit the same OAuth proxy server (same base URL / instance).","Verify the client is not caching or replaying codes; each redirect should trigger a single exchange."],"exampleFix":"// before: replaying a stored code\nconst code = savedCode; // already exchanged once\nawait exchangeToken(code);\n// after: always use the fresh code from the current redirect\nconst code = new URL(callbackUrl).searchParams.get(\"code\");\nawait exchangeToken(code);","handlingStrategy":"validation","validationCode":"// before exchanging, ensure you hold a fresh code from this redirect\nconst code = new URL(callbackUrl).searchParams.get(\"code\");\nif (!code) throw new Error(\"missing authorization code\");\nif (code === lastExchangedCode) throw new Error(\"authorization code already used\");","typeGuard":null,"tryCatchPattern":"try {\n  await exchangeAuthorizationCode(code);\n} catch (e) {\n  if (e.code === \"invalid_grant\") {\n    restartOAuthFlow(); // obtain a fresh code\n  } else throw e;\n}","preventionTips":["Never reuse or cache authorization codes; exchange each code exactly once","Use persistent/shared storage for the code store in multi-instance or restart-prone deployments","Hit the same server instance/deployment for authorize and token endpoints"],"tags":["oauth","token-exchange","invalid-grant"],"backgroundTag":"oauth-authorization-code-reuse","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}