n8n17 min readPublished October 2026

Since n8n 2.0 the OAuth callback checks who is logged in, and it is not your client

Sending a client the "Sign in with Google" link from an n8n credential used to work, for five minutes. Since 2.0 the callback is matched against the n8n user who started it, and the client's consent comes back as "Unauthorized". The variable that turns the check off is what most forum answers recommend, and what the code describes as a guard against "cross-user credential hijacking". The supported route is one the Community edition only half supports, and the same question - whose account clicked Allow - decides whether a Lead Ads trigger keeps working after you leave.

On 12 September an agency owner described on n8n's forum the handover every build ends with:

When I send the "Sign in with Google" URL to a client (they are listed as testers, as I am in the Google Cloud Console with owner permission), they get the Google permission screen ([MY ACCOUNT] wants access to your Google account), but once they clint send/accept, there's a JSON error: {status: error, message: unauthorized}.

Four days earlier, a developer building a WhatsApp agent product on n8n 2.30.4 had asked the same question in a different form - "the correct way to allow my clients to connect their own Google Calendar accounts completely remotely, without ever sharing their Google password with me" - and got a different error back, "OAuth callback state is invalid!". Eleven days before that, a Spanish speaker asked how to set up the sender "sin que el cliente tenga que venir a mi computadora física", and the first reply, from one of the forum's top supporters, was to copy the URL from the popup window and send it. On 2.x, that advice produces one of the two errors above.

It did use to work, and how it worked matters. In November 2024, n8n's 1.69 line added three things to the OAuth callback: a check that the n8n user finishing the flow is the user who started it, a five-minute expiry on the flow, and a variable to turn the first check off. The pull request that added them says why the check was off:

Auth on the callback endpoint is disabled for now for backward compatibility, but will be enabled by default in the next major release.

So on every 1.x release since then, a "Sign in with Google" link sent to a client worked if the client clicked it within five minutes. The next major release was 2.0, on 8 December 2025. In the source at tag n8n@1.123.79, the last 1.x on 12 September, the default reads ?? 'true' with the comment // TODO: Flip this flag in v2 above it. At n8n@2.0.0 it reads ?? 'false'. That is the entire change, and it is documented in one place: the 2.0 breaking changes page, which on 12 September still said n8n "will require authentication for OAuth callback endpoints by default", in the future tense, under an opening line announcing that 2.0 "has been released". That page is the only place the variable is documented: the environment-variables reference for security settings does not list N8N_SKIP_AUTH_ON_OAUTH_CALLBACK at all.

What the callback checks

The check is not, as the forum usually puts it, "the session that started it". At tag n8n@2.38.7, the stable release on 12 September, the callback handler in packages/cli/src/oauth/oauth.service.ts does this at lines 550-552:

ts
if (req.user?.id === undefined || decryptedState.userId !== req.user.id) {
    throw new AuthError('Unauthorized');
}

decryptedState.userId is the n8n user who clicked "Sign in with Google"; req.user is whoever is logged in to n8n in the browser that Google redirects back to. Google does not read the state and completes its consent screen either way. Then n8n asks two questions: is anyone logged in here, and is it the same person. A client who is not an n8n user fails the first. A client who is an n8n user but not the one who started the flow fails the second. The same n8n user from another browser passes, unless a separate, off-by-default binding option is on.

What the client sees depends on the version, which is why the two threads above quote different errors:

  • Stable releases from 2.0.0 to 2.22.x: the callback route required a login, and the middleware answered before the handler ran: 401 with {"status":"error","message":"Unauthorized"}. That is the JSON in the first thread - which puts that instance on a release before 2.23.2, or means the client paraphrased the page.
  • 2.23.2 (1 June 2026) onwards: a change merged on 26 May let the route through without a login so that a different feature could work, and the same failure now renders a page - Error: Unauthorized, then Failed to connect. The window can be closed now. - with HTTP 200. The pull request flagged it itself: "Previously, an unauthenticated callback for a static credential with the env var unset got a JSON 401 from middleware. It now gets 200 with an HTML oauth-error-callback page."
  • 2.26.4 (15 June 2026) onwards, more than five minutes after the click: the flow has expired from n8n's cache before the user check runs, and the page says The OAuth callback state is invalid!. (On earlier 2.x releases the user check came first, so a stale link from a client produced Unauthorized instead.) That is the error in the second thread, and it is the one a client gets when the link sat in an inbox for an hour - or when the link had already been opened once. The moderator who answered that thread explained the user check; the error the user quoted was the timer, or a used link.

The same error, mostly for a different reason

The client handover is the smaller share of the threads about this error. On 12 September a search of the forum for the "Unauthorized" callback error on 2.x turned up more threads from instance owners clicking "Sign in with Google" themselves and failing the same check - because req.user was empty in the browser Google redirected to - than from agencies sending links. One had opened n8n through a hosting panel's link rather than logging in at the instance's own domain; one had started the flow on a LAN address and received the callback on a public tunnel; one had started it on a loopback address through an SSH tunnel; one had a bookmark to an auto-generated domain instead of the custom one. The GitHub issue that covered the bookmark case ends with its author writing "Confirmed it was a local misconfiguration [...] so the auth cookie never matched. Not an n8n bug", after an n8n staff member had said he could not reproduce it and suspected configuration; the staff member closed it. The cookie is named n8n-auth, it is set for the hostname you logged in at, and the callback must land on that hostname to carry it.

In most of those threads somebody offered the same line, and in two it is the accepted answer: set N8N_SKIP_AUTH_ON_OAUTH_CALLBACK=true and restart. It works, in the sense that the error goes away. What it removes is the user check itself - the code path at lines 543-548 of the same file, which fetches the credential "without user" when the variable is on - leaving the CSRF signature on the state and the five-minute expiry. The comment on the field that carries the user ID, in the 1.x and 2.0.0 source, describes what the check is for: "included to prevent cross-user credential hijacking". A separate option added in June, browser binding, describes the attack it exists to close: "phishing attacks where an attacker injects an OAuth authorize URL into a victim's browser to land tokens on the attacker's credential". The May pull request kept the variable "because it's still meaningful for static credentials in iframe/embed scenarios". None of that appears in the documentation; the official sentences about the trade-off are the breaking-changes page's migration path and the migration report's "for improved security".

For the owner-not-logged-in case, the fix is the hostname, not the variable. For the client case, the variable makes the shared link work again, and every credential on the instance can then be completed by whoever holds a fresh link.

What the client can do instead

The supported route is for the client to be logged in to n8n when they click. Which means the client needs an n8n account on your instance, and how far that goes depends on the edition.

On the Community edition, user management exists - Settings → Users → Invite, and the invite link can be copied and sent without SMTP being set up. Credential sharing does not: n8n's edition comparison lists "Sharing (workflows, credentials)" as excluded, with the note "Only the instance owner and the user who creates them can access workflows and credentials". So the working arrangement, by the documentation and by the permission scopes in the code, is that the client is invited, logs in, creates the credential in their own personal space and clicks "Sign in with Google" there; the instance owner holds credential:read and credential:update globally and can use it in the workflow. The reverse arrangement also works: the client owns the instance and the agency is an invited member. What does not exist on Community is a "transfer this credential to that user" operation; the only documented path is deleting a user and choosing to "copy their workflow data and credentials to a new user" on the way out.

On Business and Enterprise, sharing and projects exist, and a credential can be created in the client's project and shared. On n8n Cloud sharing is on every plan, and the environment variable is not yours to set.

The feature whose name suggests it was built for this is end-user credentials, and the moderator in the WhatsApp thread was right to warn against it. n8n's documentation:

End-user credentials are available on: n8n Cloud: Enterprise; Self-hosted: Enterprise

Preview status - End-user credentials are in Preview and may change in future releases. Don't rely on them in production workflows.

And further down the same page, the part that rules it out for an agency's usual workflow even on Enterprise: it "lets workflows run with the credentials of the person who triggers them", the person connecting must be a user with project access, and the supported triggers are "the manual trigger, Chat Hub, the MCP Server Trigger, the n8n Form Trigger, and the Chat Trigger with Hosted Chat only". A schedule or a webhook is not on the list.

One reply in the WhatsApp thread, from a user with n8n's "Expert Partner" title, added: "Don't forget you will need a paid license to be allowed to do this." That refers to the Sustainable Use License, which allows "providing consulting services related to n8n, for example building workflows" and does not allow "hosting n8n and charging people money to access it" or making n8n "available to your customers for them to connect their accounts and build workflows". Where a multi-client product built on one instance falls between those two sentences is a licensing question, and it is a real one, but it is not the one this article is about.

The account that clicked Connect

The second question in the same week's threads is not about n8n's check at all, and it outlives it. On 10 September:

The client's Facebook account has access to the relevant Facebook Page, Meta Business account, ad account, and lead forms. My account is only being used to configure the automation as the developer. In this situation, should the Facebook Lead Ads credential be connected using: 1. The client's Facebook account, or 2. My developer account?

The moderator's answer was the client's, and the reasoning holds up against the node's source. The Facebook Lead Ads credential stores a user access token for whoever clicked Connect. On every lead event, the trigger reads the lead and its form with that user token directly (FacebookLeadAdsTrigger.node.ts, lines 262-273, tag n8n@2.38.7); a Page token is derived from the same user token only when the trigger is activated, to subscribe the app to the Page (GenericFunctions.ts, lines 166-175 and 196-208). Nothing about the Page is stored. Meta's documentation on access tokens says a Page token comes back for "Pages you have a role on" and that "the person requesting the token must have a role on the Page", and its lead-retrieval guide requires a token "requested by a person who can advertise on the ad account and on the Page". Take the developer out of the client's business portfolio, and that developer's token no longer has the Page

  • and the read fails. Meta, as far as its documentation says, keeps sending the webhook. The moderator's summary - "the day you come off the client's business portfolio that token loses lead access and the trigger goes quiet" - is a conclusion from those rules rather than a sentence Meta has written, but it is the conclusion the rules lead to. A user in an earlier thread had already put it as advice: "Never authorize with your own account 'temporarily' you'll just have to redo it later."

This is the question that does not go away with the version, and the other platforms answer it in the open. Zapier's help centre has a "Change ownership of an app connection" section for Team and Enterprise accounts, and since May its editor "checks that the Zap's owner has access to every app connection used in the Zap" before a Zap can be turned on; "if a Zap owner loses access to an app connection used in their Zap workflow, the Zap workflow pauses". Make has "credential requests" - a link a builder sends so that "the person authorizing the request enters credentials directly in Make to create the connection" - limited to partners and Enterprise customers; on other plans the path is the same as n8n's, with the client as a team member creating the connection. n8n has the check, the variable, and the delete-user dialog.

Which account holds each credential

The checks below are for an instance you are about to hand over, or one you were handed.

  1. Open the Credentials list and read the badge on each card for every OAuth credential: "Personal" is yours; anything else is the name of the user or project the credential lives in. Each one is tied to the account that clicked Sign in. Any owned by a person who will not be on the instance in six months is a handover item, whether or not anything is currently wrong.
  2. For Google credentials, check the consent screen's status as well. A credential that survives the n8n check can still expire seven days after Allow if the Google project is left in Testing; that failure has its own article, and its ten-second check is in there.
  3. Do not send the Sign in link. Invite the client as a user, have them log in at the instance's own domain - the one the n8n-auth cookie is set for, not a panel link or a tunnel address - open the credential, and click within five minutes of clicking Sign in - the clock starts on the click, not on the page load. If the error is "state is invalid", the clock ran out or the link was already used; start again. If it is "Unauthorized", the wrong person, or nobody, is logged in on that browser.
  4. For Facebook Lead Ads, connect with an account that belongs to the client's business and has a role on the Page, and keep the Meta app in the client's portfolio, since business verification attaches to the business. Connecting with the developer's account is not a shortcut; it is a deferred outage with a date nobody has written down.
  5. If you find N8N_SKIP_AUTH_ON_OAUTH_CALLBACK=true in the environment, find out why it is there. If it was added to make an owner's own login work, the cause was a hostname and the variable can come out. If it was added so clients could use shared links, decide whether everyone who can obtain a fresh authorize URL for any credential on the instance should be able to complete it, because that is what it allows.

Re-homing the credentials on an instance that was built under one account and runs under another is a few hours' work, and n8n work of this kind has a fixed price. The ones that are wrong are visible in the list; the ones that will go wrong are the Lead Ads triggers whose owner is on a different payroll.

The 1.x branch has not stopped. n8n@1.123.79 was published on 10 September, and on it the shared link still works, for five minutes, as it has since November 2024. An agency that never moved a client to 2.x has never seen the error above and has a handover procedure that stops working on the day the instance is upgraded - which on n8n Cloud is a day nobody on the client's side chooses.

Sources

Broken workflow? Fix S — $300, 2 business days, fixed price.

Get my quote in 24h

Written by the Fixmation team.