{"record":{"id":"66fcd48570c607e9","repo":"langgenius/dify","slug":"access-denied","errorCode":"access_denied","errorMessage":"authorization denied","messagePattern":"authorization denied","errorType":"error_code","errorClass":"BaseError","httpStatus":null,"severity":"error","filePath":"cli/src/commands/auth/login/device-flow.ts","lineNumber":54,"sourceCode":"    device_code: code.device_code,\n    client_id: opts.clientId ?? DEFAULT_CLIENT_ID,\n  }\n\n  while (true) {\n    if (opts.clock.isCancelled()) throw expired()\n    const result = await pollWithRetry(api, req, opts.clock)\n    switch (result.status) {\n      case 'approved':\n        return result.success\n      case 'pending':\n        break\n      case 'slow_down':\n        interval = Math.min(interval * 2, MAX_INTERVAL_MS)\n        break\n      case 'expired':\n        throw expired()\n      case 'denied':\n        throw new BaseError({\n          code: ErrorCode.AccessDenied,\n          message: 'authorization denied',\n        })\n      case 'retry_5xx':\n        throw new BaseError({\n          code: ErrorCode.Server5xx,\n          message: 'device-flow poll unavailable after retries',\n        })\n    }\n    await opts.clock.sleepMs(interval)\n    if (opts.clock.isCancelled()) throw expired()\n  }\n}\n\nasync function pollWithRetry(\n  api: DeviceFlowApiSubset,\n  req: PollRequest,\n  clock: Clock,","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/langgenius/dify/blob/ef8544b173fd6cd7a8e71df2cab576e52bebbfbc/cli/src/commands/auth/login/device-flow.ts#L36-L72","documentation":"Raised by DatasourceOAuthCallback.get (GET /oauth/plugin/{provider_id}/datasource/callback) when neither the context_id cookie nor the context_id query parameter is present. The callback is browser-redirected from the OAuth provider, which normally carries the context_id cookie set by the get-authorization-url endpoint. Its absence means the cookie was never set, was cleared, or the callback was invoked directly. Maps to HTTP 403 via werkzeug Forbidden.","triggerScenarios":"OAuth provider redirects back to /callback but the user's browser dropped the context_id cookie (third-party cookie blocking, different domain, incognito); a developer manually opens the callback URL in a new tab without first hitting get-authorization-url; the cookie expired (max_age = OAuthProxyService.__MAX_AGE__).","commonSituations":"Browser blocks third-party/same-site cookies because the OAuth provider redirects across origins; Safari ITP stripping the cookie; user copied the callback URL instead of following the redirect flow; long delay between authorization and callback exceeding cookie max_age.","solutions":["Always initiate the flow via GET /oauth/plugin/{provider_id}/datasource/get-authorization-url so the context_id cookie is set first.","Ensure the browser accepts first-party cookies for the console domain (check SameSite=Lax cookie behavior).","If the cookie expired, restart the OAuth flow from the authorization-url endpoint.","For programmatic testing, pass context_id as a query parameter matching one obtained from create_proxy_context."],"exampleFix":"// before — opening the callback URL directly\nwindow.location = `/console/api/oauth/plugin/${providerId}/datasource/callback?code=${code}`;\n// after — start from the authorization-url endpoint so the cookie is set\nwindow.location = `/console/api/oauth/plugin/${providerId}/datasource/get-authorization-url`;","handlingStrategy":"validation","validationCode":"function hasContextId(): boolean {\n  return Boolean(getCookie('context_id') || new URLSearchParams(location.search).get('context_id'));\n}\nif (!hasContextId()) {\n  // start the OAuth flow from the top\n  window.location = `/console/api/oauth/plugin/${providerId}/datasource/get-authorization-url`;\n}","typeGuard":"function isOAuthInitiated(contextId: string | null | undefined): contextId is string {\n  return typeof contextId === 'string' && contextId.length > 0;\n}","tryCatchPattern":"try {\n  await fetch(callbackUrl);\n} catch (e) {\n  if (e.response?.status === 403 && /context_id not found/i.test(e.response.data?.message || '')) {\n    // restart the flow to set the cookie\n    window.location = `/console/api/oauth/plugin/${providerId}/datasource/get-authorization-url`;\n    return;\n  }\n  throw e;\n}","preventionTips":["Always begin OAuth from the get-authorization-url endpoint so the context_id cookie is set.","Ensure the browser accepts first-party cookies for the console domain.","Never deep-link the callback URL; it requires the cookie from the initiation step."],"tags":["oauth","callback","cookie","forbidden","authentication","console"],"backgroundTag":null,"analyzedSha":"ef8544b173fd6cd7a8e71df2cab576e52bebbfbc","analyzedAt":"2026-08-12T05:15:17.394Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}