{"record":{"id":"cb3de528ff7027ab","repo":"PrefectHQ/fastmcp","slug":"invalid-grant-cb3de5","errorCode":"invalid_grant","errorMessage":"invalid_grant: Authorization code not found or already used.","messagePattern":"invalid_grant: Authorization code not found or already used\\.","errorType":"error_code","errorClass":"TokenError","httpStatus":400,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/providers/in_memory.py","lineNumber":175,"sourceCode":"        auth_code_obj = self.auth_codes.get(authorization_code)\n        if auth_code_obj:\n            if auth_code_obj.client_id != client.client_id:\n                return None  # Belongs to a different client\n            if auth_code_obj.expires_at < time.time():\n                del self.auth_codes[authorization_code]  # Expired\n                return None\n            return auth_code_obj\n        return None\n\n    async def exchange_authorization_code(\n        self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode\n    ) -> OAuthToken:\n        # Authorization code should have been validated (existence, expiry, client_id match)\n        # by the TokenHandler calling load_authorization_code before this.\n        # We might want to re-verify or simply trust it's valid.\n\n        if authorization_code.code not in self.auth_codes:\n            raise TokenError(\n                \"invalid_grant\", \"Authorization code not found or already used.\"\n            )\n\n        # Consume the auth code\n        del self.auth_codes[authorization_code.code]\n\n        access_token_value = f\"test_access_token_{secrets.token_hex(32)}\"\n        refresh_token_value = f\"test_refresh_token_{secrets.token_hex(32)}\"\n\n        access_token_expires_at = int(time.time() + DEFAULT_ACCESS_TOKEN_EXPIRY_SECONDS)\n\n        # Refresh token expiry\n        refresh_token_expires_at = None\n        if DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS is not None:\n            refresh_token_expires_at = int(\n                time.time() + DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS\n            )\n","sourceCodeStart":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/providers/in_memory.py#L157-L193","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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"],"exampleFix":"// before\nawait provider.exchange_authorization_code(client, code)  # succeeds, code deleted\nawait provider.exchange_authorization_code(client, code)  # invalid_grant\n// after\ntokens = await provider.exchange_authorization_code(client, code)  # call once, store tokens\n# new flow needed for another code:","handlingStrategy":"try-catch","validationCode":"if authorization_code.code not in provider.auth_codes:\n    # code consumed or never issued — restart the authorization flow\n    authorization_code = await start_authorization_flow(client)","typeGuard":"def code_is_fresh(provider, code) -> bool:\n    return code.code in provider.auth_codes","tryCatchPattern":"try:\n    tokens = await provider.exchange_authorization_code(client, authorization_code)\nexcept TokenError as e:\n    if e.error == \"invalid_grant\":\n        tokens = await restart_authorization_flow(client)  # fresh code, single-use","preventionTips":["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"],"tags":["oauth","authorization-code","token-exchange","single-use"],"backgroundTag":"invalid-grant","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}