langgenius/dify · error · BaseError

access_denied

access_denied

Error message

authorization denied

What it means

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.

Solutions

  1. Always initiate the flow via GET /oauth/plugin/{provider_id}/datasource/get-authorization-url so the context_id cookie is set first.
  2. Ensure the browser accepts first-party cookies for the console domain (check SameSite=Lax cookie behavior).
  3. If the cookie expired, restart the OAuth flow from the authorization-url endpoint.
  4. For programmatic testing, pass context_id as a query parameter matching one obtained from create_proxy_context.

Example fix

// before — opening the callback URL directly
window.location = `/console/api/oauth/plugin/${providerId}/datasource/callback?code=${code}`;
// after — start from the authorization-url endpoint so the cookie is set
window.location = `/console/api/oauth/plugin/${providerId}/datasource/get-authorization-url`;
Defensive patterns

Strategy: validation

Validate before calling

function hasContextId(): boolean {
  return Boolean(getCookie('context_id') || new URLSearchParams(location.search).get('context_id'));
}
if (!hasContextId()) {
  // start the OAuth flow from the top
  window.location = `/console/api/oauth/plugin/${providerId}/datasource/get-authorization-url`;
}

Type guard

function isOAuthInitiated(contextId: string | null | undefined): contextId is string {
  return typeof contextId === 'string' && contextId.length > 0;
}

Try / catch

try {
  await fetch(callbackUrl);
} catch (e) {
  if (e.response?.status === 403 && /context_id not found/i.test(e.response.data?.message || '')) {
    // restart the flow to set the cookie
    window.location = `/console/api/oauth/plugin/${providerId}/datasource/get-authorization-url`;
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: 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__).

Common situations: 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.

Related errors


AI-assisted analysis of langgenius/dify@ef8544b173 (2026-08-12). Data as JSON: /api/errors/66fcd48570c607e9. Report an issue: GitHub.

Appendix: source

Thrown at cli/src/commands/auth/login/device-flow.ts:54

    device_code: code.device_code,
    client_id: opts.clientId ?? DEFAULT_CLIENT_ID,
  }

  while (true) {
    if (opts.clock.isCancelled()) throw expired()
    const result = await pollWithRetry(api, req, opts.clock)
    switch (result.status) {
      case 'approved':
        return result.success
      case 'pending':
        break
      case 'slow_down':
        interval = Math.min(interval * 2, MAX_INTERVAL_MS)
        break
      case 'expired':
        throw expired()
      case 'denied':
        throw new BaseError({
          code: ErrorCode.AccessDenied,
          message: 'authorization denied',
        })
      case 'retry_5xx':
        throw new BaseError({
          code: ErrorCode.Server5xx,
          message: 'device-flow poll unavailable after retries',
        })
    }
    await opts.clock.sleepMs(interval)
    if (opts.clock.isCancelled()) throw expired()
  }
}

async function pollWithRetry(
  api: DeviceFlowApiSubset,
  req: PollRequest,
  clock: Clock,

View on GitHub (pinned to ef8544b173)