{"record":{"id":"e64eebcf40bde7fa","repo":"Significant-Gravitas/AutoGPT","slug":"state-token-was-not-created-for-external-oauth-flo","errorCode":null,"errorMessage":"State token was not created for external OAuth flow","messagePattern":"State token was not created for external OAuth flow","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"autogpt_platform/backend/backend/api/external/v1/integrations.py","lineNumber":426,"sourceCode":"    This endpoint should be called after the user has authorized the application\n    and been redirected back to the external app's callback URL with an\n    authorization code.\n    \"\"\"\n    # Verify state token\n    valid_state = await creds_manager.store.verify_state_token(\n        auth.user_id, request.state_token, provider\n    )\n\n    if not valid_state:\n        logger.warning(f\"Invalid or expired state token for provider {provider}\")\n        raise HTTPException(\n            status_code=status.HTTP_400_BAD_REQUEST,\n            detail=\"Invalid or expired state token\",\n        )\n\n    # Verify this is an external flow (callback_url must be set)\n    if not valid_state.callback_url:\n        raise HTTPException(\n            status_code=status.HTTP_400_BAD_REQUEST,\n            detail=\"State token was not created for external OAuth flow\",\n        )\n\n    # Get OAuth handler with the original callback URL\n    handler = _get_oauth_handler_for_external(provider, valid_state.callback_url)\n\n    try:\n        scopes = valid_state.scopes\n        scopes = handler.handle_default_scopes(scopes)\n\n        credentials = await handler.exchange_code_for_tokens(\n            request.code, scopes, valid_state.code_verifier\n        )\n\n        # Handle Linear's space-separated scopes\n        if len(credentials.scopes) == 1 and \" \" in credentials.scopes[0]:\n            credentials.scopes = credentials.scopes[0].split(\" \")","sourceCodeStart":408,"sourceCodeEnd":444,"githubUrl":"https://github.com/Significant-Gravitas/AutoGPT/blob/9c8bb5550f446ba5d3046b78896578742495b3cf/autogpt_platform/backend/backend/api/external/v1/integrations.py#L408-L444","documentation":"Raised (HTTP 400) by the external OAuth complete endpoint when the state token itself is valid but has no `callback_url` attached. Only state tokens created by the external authorize endpoint (which stores the external callback URL in the state metadata) are accepted here; states created by the platform's internal OAuth flow have `callback_url=None` and are rejected.","triggerScenarios":"POST `/api/external-api/v1/integrations/{provider}/oauth/callback` with a state token that came from the platform's own (internal) OAuth initiation instead of the external `/oauth/authorize` endpoint; or a state stored before the external-flow metadata field existed.","commonSituations":"Mixing the internal platform OAuth endpoints with the external API endpoints in one integration script; copying state examples from internal-flow docs; stale state rows persisted by an older backend version that lacked `callback_url` in `OAuthState`.","solutions":["Always create state via POST `/integrations/{provider}/oauth/authorize` with a `callback_url` before calling the external callback endpoint.","Do not reuse state tokens obtained from the platform's internal login/OAuth flow.","If stale states persist from an older version, restart the flow to generate a new external state."],"exampleFix":"# before\nstate = start_internal_oauth(provider)          # no callback_url\nPOST /integrations/{provider}/oauth/callback {\"state_token\": state.token, ...}  # 400\n\n# after\nresp = POST /integrations/{provider}/oauth/authorize {\"callback_url\": \"https://app.example.com/cb\"}\nPOST /integrations/{provider}/oauth/callback {\"state_token\": resp.state_token, \"code\": \"...\"}","handlingStrategy":"validation","validationCode":"# Only states created by the EXTERNAL authorize endpoint have a callback_url.\n# Track provenance client-side:\nstate = authorize_external(provider, callback_url)  # marks state as external\nassert state.origin == \"external\", \"use /oauth/authorize with callback_url, not internal flow\"","typeGuard":null,"tryCatchPattern":"try:\n    client.post(f\"/integrations/{provider}/oauth/callback\", json=cb)\nexcept HTTPError as e:\n    if e.response.status_code == 400 and \"external OAuth flow\" in e.response.text:\n        raise FlowError(\"state came from internal flow; re-initiate via external authorize\") from e\n    raise","preventionTips":["Use one code path (external authorize) for external-app integrations; never mix in internal-flow states.","Tag stored states with their flow type client-side to catch mismatches early."],"tags":["oauth","state-token","flow-mismatch"],"backgroundTag":null,"analyzedSha":"9c8bb5550f446ba5d3046b78896578742495b3cf","analyzedAt":"2026-08-14T17:17:21.957Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}