Google BigQuery Node – 403 Forbidden (OAuth2 & Service Account) – All IAM, Roles, and Policies Correct, Works Everywhere Else

Problem / Issue

Whenever I use the Google BigQuery node in n8n (v1.98.2, Docker, self-hosted), all API requests to BigQuery return 403 Forbidden – specifically when fetching the project list or executing a query.
The error is always:

403. That’s an error. Your client does not have permission to get URL /bigquery/v2/projects from this server. That’s all we know.

This happens with both:

  • OAuth2 credentials
  • Service Account credentials

Desired Outcome

  • The Google BigQuery node in n8n should be able to authenticate with my Google Cloud project and execute queries or list datasets, just like the gcloud CLI, bq CLI, or other tools (make.com,etc).
  • The node should not return 403 errors if IAM roles, policies, and scopes are correct.

What I’ve Tried / Debug Info

Setup

  • n8n v1.98.2 (Docker, self-hosted, behind Caddy reverse proxy)
  • Google BigQuery Node v2.1
  • Tried multiple credentials (OAuth2, Service Account)

Accounts, IAM & Policies

  • User account is Owner and BigQuery Admin on project and organization level
  • Service account is BigQuery Admin and Owner
  • Confirmed all relevant scopes are enabled:
    • https://www.googleapis.com/auth/bigquery
    • https://www.googleapis.com/auth/cloud-platform
    • https://www.googleapis.com/auth/drive
  • Organization policy (constraints/iam.allowedPolicyMemberDomains) allows my domain, I tested with internal and external users

Testing on Other Tools

  • gcloud CLI: Works perfectly (OAuth2 login, same account)
  • bq CLI: Works perfectly
  • Google Cloud Console: Works perfectly
  • make.com: Works perfectly with both OAuth2 and Service Account

n8n Specifics

  • Clean Docker install, wiped all old data/volumes
  • Tried with the latest and multiple n8n versions (incl. fresh pull)
  • All credentials created fresh, multiple times
  • Debug logs show that the token is being sent correctly in the request:

Problem

  • The node fails before the query is run, already when fetching the project list (/bigquery/v2/projects)
  • Manual SQL insert via BigQuery UI works; fails only in n8n
  • Even giving the account Owner or Org Admin roles doesn’t help
  • Tested both “internal” and “external” OAuth with test account
  • Added both a primary (company domain) and test user (external domain) as BigQuery Admin, Job User, and Org Admin.
  • Verified permissions via CLI and web: All BigQuery actions work outside n8n (e.g., bq CLI and Google UI).

What I see in the logs

  • n8n debug logs show the outgoing request is built correctly, tokens are sent, scopes are correct
  • Error response is always the standard Google 403 HTML

What am I missing?

  • Is there any special requirement for n8n’s Google BigQuery node (headers, callback URL, app verification)?
  • Is there a known bug or incompatibility in recent n8n versions or with Google’s API?
  • Are there extra IAM/Google Cloud Platform settings needed just for API clients like n8n?
  • Any way to get deeper error info from n8n or Google API (not just the HTML error)?

References / Screenshots





  • (more on request)

Summary

TL;DR:
I’ve spent 8+ hours troubleshooting – in every other tool BigQuery works (make.com, CLI), but n8n’s node always gets a 403. All roles, scopes, service accounts, and policies are correct. This feels like a bug in n8n or the BigQuery node.

If you need more info, let me know!
Please advise if I am missing anything or if this is a known issue.

DEBUG LOG


n8n_1    | 2025-06-19T22:37:22.802Z router dispatching POST /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.802Z router <anonymous>  : /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.802Z router sentryRequestMiddleware  : /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.802Z router sentryErrorMiddleware  : /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.802Z router compression  : /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.803Z router rawBodyReader  : /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.806Z router <anonymous>  : /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.806Z router bodyParser  : /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.808Z router router  : /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.808Z router dispatching POST /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.808Z router <anonymous>  : /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.809Z router <anonymous>  : /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.809Z router cookieParser  : /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.809Z router trim prefix (/rest) from url /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.809Z router router /rest : /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.810Z router dispatching POST /dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.810Z router trim prefix (/rest/dynamic-node-parameters) from url /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.810Z router router /rest/dynamic-node-parameters : /rest/dynamic-node-parameters/resource-locator-results
n8n_1    | 2025-06-19T22:37:22.810Z router dispatching POST /resource-locator-results
n8n_1    | 2025-06-19T22:37:22.955Z follow-redirects options {
n8n_1    |   maxRedirects: 21,
n8n_1    |   maxBodyLength: Infinity,
n8n_1    |   protocol: 'https:',
n8n_1    |   path: '/bigquery/v2/projects',
n8n_1    |   method: 'GET',
n8n_1    |   headers: [Object: null prototype] {
n8n_1    |     Accept: 'application/json',
n8n_1    |     'Content-Type': 'application/json',
n8n_1    |     Authorization: 'Bearer ya29...not that stupid...0206',
n8n_1    |     'User-Agent': 'axios/1.8.3',
n8n_1    |     'Accept-Encoding': 'gzip, compress, deflate, br'
n8n_1    |   },
n8n_1    |   agents: {
n8n_1    |     http: undefined,
n8n_1    |     https: Agent {
n8n_1    |       _events: [Object: null prototype],
n8n_1    |       _eventsCount: 2,
n8n_1    |       _maxListeners: undefined,
n8n_1    |       defaultPort: 443,
n8n_1    |       protocol: 'https:',
n8n_1    |       options: [Object: null prototype],
n8n_1    |       requests: [Object: null prototype] {},
n8n_1    |       sockets: [Object: null prototype] {},
n8n_1    |       freeSockets: [Object: null prototype] {},
n8n_1    |       keepAliveMsecs: 1000,
n8n_1    |       keepAlive: false,
n8n_1    |       maxSockets: Infinity,
n8n_1    |       maxFreeSockets: 256,
n8n_1    |       scheduling: 'lifo',
n8n_1    |       maxTotalSockets: Infinity,
n8n_1    |       totalSocketCount: 0,
n8n_1    |       maxCachedSessions: 100,
n8n_1    |       _sessionCache: [Object],
n8n_1    |       [Symbol(shapeMode)]: false,
n8n_1    |       [Symbol(kCapture)]: false
n8n_1    |     }
n8n_1    |   },
n8n_1    |   auth: undefined,
n8n_1    |   family: undefined,
n8n_1    |   beforeRedirect: [Function: dispatchBeforeRedirect],
n8n_1    |   beforeRedirects: { proxy: [Function: beforeRedirect], config: [Function (anonymous)] },
n8n_1    |   hostname: 'bigquery.googleapis.com',
n8n_1    |   port: '',
n8n_1    |   agent: Agent {
n8n_1    |     _events: [Object: null prototype] {
n8n_1    |       free: [Function (anonymous)],
n8n_1    |       newListener: [Function: maybeEnableKeylog]
n8n_1    |     },
n8n_1    |     _eventsCount: 2,
n8n_1    |     _maxListeners: undefined,
n8n_1    |     defaultPort: 443,
n8n_1    |     protocol: 'https:',
n8n_1    |     options: [Object: null prototype] {
n8n_1    |       servername: 'bigquery.googleapis.com',
n8n_1    |       noDelay: true,
n8n_1    |       path: null
n8n_1    |     },
n8n_1    |     requests: [Object: null prototype] {},
n8n_1    |     sockets: [Object: null prototype] {},
n8n_1    |     freeSockets: [Object: null prototype] {},
n8n_1    |     keepAliveMsecs: 1000,
n8n_1    |     keepAlive: false,
n8n_1    |     maxSockets: Infinity,
n8n_1    |     maxFreeSockets: 256,
n8n_1    |     scheduling: 'lifo',
n8n_1    |     maxTotalSockets: Infinity,
n8n_1    |     totalSocketCount: 0,
n8n_1    |     maxCachedSessions: 100,
n8n_1    |     _sessionCache: { map: {}, list: [] },
n8n_1    |     [Symbol(shapeMode)]: false,
n8n_1    |     [Symbol(kCapture)]: false
n8n_1    |   },
n8n_1    |   nativeProtocols: {
n8n_1    |     'http:': {
n8n_1    |       _connectionListener: [Function: connectionListener],
n8n_1    |       METHODS: [Array],
n8n_1    |       STATUS_CODES: [Object],
n8n_1    |       Agent: [Function],
n8n_1    |       ClientRequest: [Function: ClientRequest],
n8n_1    |       IncomingMessage: [Function: IncomingMessage],
n8n_1    |       OutgoingMessage: [Function: OutgoingMessage],
n8n_1    |       Server: [Function: Server],
n8n_1    |       ServerResponse: [Function: ServerResponse],
n8n_1    |       createServer: [Function: createServer],
n8n_1    |       validateHeaderName: [Function],
n8n_1    |       validateHeaderValue: [Function],
n8n_1    |       get: [Function: get],
n8n_1    |       request: [Function: request],
n8n_1    |       setMaxIdleHTTPParsers: [Function: setMaxIdleHTTPParsers],
n8n_1    |       maxHeaderSize: [Getter],
n8n_1    |       globalAgent: [Getter/Setter],
n8n_1    |       WebSocket: [Getter],
n8n_1    |       CloseEvent: [Getter],
n8n_1    |       MessageEvent: [Getter]
n8n_1    |     },
n8n_1    |     'https:': {
n8n_1    |       Agent: [Function: Agent],
n8n_1    |       globalAgent: [Agent],
n8n_1    |       Server: [Function: Server],
n8n_1    |       createServer: [Function: createServer],
n8n_1    |       get: [Function: get],
n8n_1    |       request: [Function: request]
n8n_1    |     }
n8n_1    |   }
n8n_1    | }
n8n_1    | 403 - "<!DOCTYPE html>\n<html lang=en>\n  <meta charset=utf-8>\n  <meta name=viewport content=\"initial-scale=1, minimum-scale=1, width=device-width\">\n  <title>Error 403 (Forbidden)!!1</title>\n  <style>\n    *{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}\n  </style>\n  <a href=//www.google.com/><span id=logo aria-label=Google></span></a>\n  <p><b>403.</b> <ins>That’s an error.</ins>\n  <p>Your client does not have permission to get URL <code>/bigquery/v2/projects</code> from this server.  <ins>That’s all we know.</ins>\n"