{"record":{"id":"92b157d286ce895f","repo":"bytedance/deer-flow","slug":"missing-code-or-state-parameter","errorCode":null,"errorMessage":"Missing code or state parameter","messagePattern":"Missing code or state parameter","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"backend/app/gateway/routers/auth.py","lineNumber":767,"sourceCode":"\n    # ── Provider error ───────────────────────────────────────────────\n    if error:\n        logger.warning(\"OIDC provider returned error for %s: %s (description: %s)\", provider, error, error_description)\n        redirect = _build_error_redirect(oidc_config.frontend_base_url, \"sso_failed\")\n        return RedirectResponse(url=redirect, status_code=status.HTTP_302_FOUND)\n\n    if not oidc_config.enabled:\n        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=\"SSO authentication is not enabled\")\n\n    if not _OIDC_PROVIDER_KEY_RE.match(provider):\n        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=\"Invalid provider ID\")\n\n    provider_config = oidc_config.providers.get(provider)\n    if not provider_config:\n        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=f\"Unknown SSO provider: {provider}\")\n\n    if not code or not state:\n        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=\"Missing code or state parameter\")\n\n    # ── Verify state cookie ──────────────────────────────────────────\n    state_payload = get_state_cookie(request, provider)\n    if not state_payload:\n        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=\"Missing or expired OIDC state cookie\")\n\n    if not secrets.compare_digest(state_payload.state, state):\n        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=\"OIDC state mismatch\")\n\n    # ── Resolve redirect URI ─────────────────────────────────────────\n    redirect_uri = _resolve_oidc_redirect_uri(request, provider, provider_config)\n\n    # ── Get metadata ─────────────────────────────────────────────────\n    overrides = {\n        \"authorization_endpoint\": provider_config.authorization_endpoint,\n        \"token_endpoint\": provider_config.token_endpoint,\n        \"userinfo_endpoint\": provider_config.userinfo_endpoint,\n        \"jwks_uri\": provider_config.jwks_uri,","sourceCodeStart":749,"sourceCodeEnd":785,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/routers/auth.py#L749-L785","documentation":"400 Bad Request from the SSO callback handler (auth.py:767) when the IdP's redirect back to /api/auth/sso/{provider}/callback lacks either the 'code' or the 'state' query parameter. In a healthy authorization-code flow the IdP always returns both; their absence means the callback URL was hand-built, mangled by a proxy, or the IdP is misbehaving (an IdP-side failure normally comes back as 'error'/'error_description' params, which are handled earlier with a 302 to 'sso_failed').","triggerScenarios":"Direct navigation/bookmark of the callback URL with no query string; a reverse proxy or frontend router stripping query parameters during the redirect; a misconfigured IdP redirect URI template that drops ?code=...&state=...","commonSituations":"Nginx/proxy rewrite losing the query string (proxy_pass without URI vs with URI pitfalls); testing the callback by pasting the bare URL into a browser; WAF scrubbing query params.","solutions":["Re-run the login flow from the start route and capture the exact redirect Location the IdP emits — confirm it carries both code and state","Fix any proxy/rewrite rule that drops query strings on the callback path (avoid proxy_pass with a path component that discards the original URI query)","Verify the IdP app's redirect URI matches the Gateway callback route exactly so the IdP appends parameters correctly"],"exampleFix":"# nginx (before) — path-style proxy_pass drops query handling subtleties\nlocation /api/auth/sso/ {\n    proxy_pass http://gateway:8001/api/auth/sso;  # trailing-slash mismatch\n}\n\n# after — clean pass-through preserving the full URI + query\nlocation /api/auth/sso/ {\n    proxy_pass http://gateway:8001;\n}","handlingStrategy":"validation","validationCode":"# Validate the IdP redirect Location before following it\nloc = idp_response.headers[\"location\"]\nfrom urllib.parse import urlparse, parse_qs\nq = parse_qs(urlparse(loc).query)\nassert \"code\" in q and \"state\" in q, f\"callback missing params: {loc}\"","typeGuard":null,"tryCatchPattern":"try { await callback() } catch (e) { if (e.status === 400 && /Missing code or state/.test(e.detail)) restartSsoFlow(); else throw e; }","preventionTips":["Never bookmark or directly construct the callback URL; always enter via the start route","Test the full redirect chain in staging to catch query-string-dropping proxies","Check the 'error' query param first in your own handlers — IdP failures arrive that way, not as missing code/state"],"tags":["sso","oidc","validation","http-400","proxy"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}