{"record":{"id":"f6c5cdeb757d6ee9","repo":"Significant-Gravitas/AutoGPT","slug":"invalid-or-expired-state-token","errorCode":null,"errorMessage":"Invalid or expired state token","messagePattern":"Invalid or expired state token","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"autogpt_platform/backend/backend/api/external/v1/integrations.py","lineNumber":419,"sourceCode":"    auth: APIAuthorizationInfo = Security(\n        require_permission(APIKeyPermission.MANAGE_INTEGRATIONS)\n    ),\n) -> OAuthCompleteResponse:\n    \"\"\"\n    Complete an OAuth flow by exchanging the authorization code for tokens.\n\n    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","sourceCodeStart":401,"sourceCodeEnd":437,"githubUrl":"https://github.com/Significant-Gravitas/AutoGPT/blob/9c8bb5550f446ba5d3046b78896578742495b3cf/autogpt_platform/backend/backend/api/external/v1/integrations.py#L401-L437","documentation":"Raised (HTTP 400) by the external OAuth complete endpoint when `creds_manager.store.verify_state_token(user_id, state_token, provider)` returns None. Verification is a constant-time token comparison plus a provider match and an expiry check; a valid state is also single-use (it is removed on success), so replaying a completed flow fails the same way.","triggerScenarios":"POST `/api/external-api/v1/integrations/{provider}/oauth/callback` with a state_token that is expired (past `expires_at`), already consumed, issued for a different provider, issued for a different user, or corrupted/missing. Calling the callback endpoint twice with the same state is the classic replay case.","commonSituations":"User sits on the provider consent screen longer than the state TTL; double-submit of the callback (refresh or retry after a network error); state serialized incorrectly by the external app (truncation/encoding); mixing states between the internal platform OAuth flow and the external flow.","solutions":["Re-initiate the flow via the authorize endpoint to get a fresh state token and complete the callback promptly.","Make the callback idempotent-safe on the client: never retry a used state; on 400 'Invalid or expired state token' always restart from authorize.","Verify you pass the same provider in the URL as was used at initiation (provider must match `provider_matches`).","Check the state token is transmitted unmodified (no trimming, re-encoding, or JSON escaping issues)."],"exampleFix":"# before: reusing an old/consumed state\nPOST /integrations/github/oauth/callback\n{\"state_token\": \"st_used_or_expired\", \"code\": \"...\"}  # 400\n\n# after: restart the flow\nPOST /integrations/github/oauth/authorize  -> new state_token\nPOST /integrations/github/oauth/callback {\"state_token\": new, \"code\": \"...\"}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try:\n    result = client.post(f\"/integrations/{provider}/oauth/callback\", json={\"state_token\": state, \"code\": code})\nexcept HTTPError as e:\n    if e.response.status_code == 400 and \"state token\" in e.response.text.lower():\n        # state is single-use and expiring: always restart the flow\n        state = start_new_flow(provider)\n        result = client.post(f\"/integrations/{provider}/oauth/callback\", json={\"state_token\": state, \"code\": new_code})\n    else:\n        raise","preventionTips":["Complete the callback immediately after the provider redirect; states expire and are single-use.","Never retry a callback with the same state token; on 400 restart from authorize.","Keep provider consistent between authorize and callback calls."],"tags":["oauth","state-token","csrf","expiry"],"backgroundTag":null,"analyzedSha":"9c8bb5550f446ba5d3046b78896578742495b3cf","analyzedAt":"2026-08-14T17:17:21.957Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}