# Best practices for OAuth 2.0 APIs with login, access tokens, and refresh tokens in n8n?

**URL:** <https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096>\
**Category:** Questions\
**Tags:** credentials, http-request, oauth2, bug:unconfirmed\
**Created:** [October 13, 2026, 10:51am UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096 "2026-10-13T10:51:26Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![axiltia](https://avatars.discourse-cdn.com/v4/letter/a/848f3c/32.png) [@axiltia](https://community.n8n.io/u/axiltia)\
**Post date:** [October 13, 2026, 10:51am UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/1 "2026-10-13T10:51:26Z")

</div>

Hi everyone,

How do you handle authentication in n8n for APIs such as [PlentyONE](https://developers.plentymarkets.com/en-gb/developers/main/rest-api-guides/getting-started.html#_authentication) that use the following flow?

PlentyONE REST API uses **OAuth 2.0**.

- Log in by sending a username and password in the JSON request body.

- Receive `access_token`, `refresh_token`, and `expires_in`.

- Use a separate endpoint to renew the access token using the refresh token.

Logging in through an HTTP Request node with a configured credential works. However, I want to avoid logging in and requesting a new token on every workflow execution.

**The goal is to log in once and reuse the tokens across multiple executions.** A scheduled or manual execution should reuse the existing access token while it is valid. When renewal is necessary, the workflow should use the refresh token. A new login should only be necessary when no valid refresh token is available.

I built the workflow with ChatGPT/Codex. The AI suggested the following approach, which I would like to check against the community’s experience before using it in production:

 ![exec-84b58979-4c06-4d7c-ab9b-19a3eabc8f4e](https://europe1.discourse-cdn.com/n8n/original/3X/5/0/506da586cb16f6fb0152aead1574c04eb17ac9b2.jpeg)

The username and password are stored in a Custom Auth credential. The access token, refresh token, and expiration time are stored in an n8n Data Table. Valid tokens are reused and refreshed when needed. If the refresh token is invalid, the workflow attempts to log in again automatically.

Personally, **storing the tokens in a N8N Data Table does not feel particularly elegant**. I expected token management to be handled within the credentials, but I do not have enough experience to judge which approach is standard or recommended.

**What is considered best practice for this scenario in n8n?** Is managing tokens within the workflow common, or can the entire authentication lifecycle be handled by a credential integration?

I am particularly interested in secure token storage, concurrent executions, and reliable error handling. I cannot find a suitable grant type in the generic OAuth2 credential for the login method described above.

The attached diagram illustrates the proposed approach.

Thanks for sharing your experience!

**Environment:** n8n Cloud, Starter plan  
**Version:** 2.43.4

# Debug info

## core

- n8nVersion: 2.43.4
- platform: docker (cloud)
- nodeJsVersion: 26.7.0
- nodeEnv: production
- database: sqlite
- executionMode: regular

---

<div class="post-metadata">

**Author:** ![n8n](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/n8n/32/196425_2.png) [@n8n](https://community.n8n.io/u/n8n)\
**Post date:** [October 13, 2026, 10:52am UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/2 "2026-10-13T10:52:16Z")

</div>

Hey @axiltia, while you wait for a response, here are some things that might help:

> **Suggested resources**
>
> _Automatically matched to your question._
> 
> **Docs:**
> 
> - [getWorkflowStaticData](https://docs.n8n.io/build/code-in-n8n/cookbook/built-in-methods-and-variables-examples/getworkflowstaticdata)
> - [HTTP Request credentials](https://docs.n8n.io/integrations/builtin/credentials/httprequest)
> - [HTTP Request credentials](https://docs.n8n.io/integrations/builtin/credentials/httprequest#using-oauth2)
> 
> **Forum:**
> 
> - [Getting a generated token](https://community.n8n.io/t/295822)
> - [Zoho WorkDrive Generic OAuth2 – Refresh Token not working](https://community.n8n.io/t/279454)
> - [How to implement custom n8n node authentication with API key that needs auto-refresh](https://community.n8n.io/t/194189)

@russellkg, @Chris360karat, @krisn0x - you’ve helped with similar issues before, can you take a look?

_Automatically suggested by n8n’s community bot. It’s a pilot - [please share feedback here](https://community.n8n.io/t/pilot-forum-helper-bot/304669)._

---

<div class="post-metadata">

**Author:** ![Loopwise\_AI\_Loopwise](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/loopwise_ai_loopwise/32/193570_2.png) [@Loopwise\_AI\_Loopwise](https://community.n8n.io/u/Loopwise_AI_Loopwise)\
**Post date:** [October 13, 2026, 11:08am UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/3 "2026-10-13T11:08:34Z")

</div>

Your Data Table approach is actually a reasonable pattern for this kind of API — there’s no built-in credential type that handles “login with username/password in a JSON body → access/refresh token” automatically.

**Why the built-in credentials don’t cover it**

- The generic **OAuth2 API** credential supports Authorization Code, Client Credentials and PKCE grants. A custom `/login` endpoint that takes username/password in the body isn’t one of those, so n8n can’t manage the refresh lifecycle for you there.
- Header/Custom Auth credentials only hold static values — they can’t store a token that changes over time.

**Options, from simplest to cleanest**

1. **Keep tokens in a Data Table (what you have)**  
It survives across executions _and_ works for manual runs, which matters: `$getWorkflowStaticData()` is only persisted for production (trigger/active) executions, not when you test manually, so it’s less predictable while developing.  
Two things worth adding:

2. **Move the token logic into one sub-workflow (“Get PlentyONE token”)**  
Data Table lookup → valid? return it : refresh → refresh failed? login → upsert row → return `access_token`. Every other workflow just calls it with **Execute Workflow** and uses `{{ $json.access_token }}` in the Authorization header. One place to maintain, and all workflows share the same token instead of each logging in separately.  
If several executions can run at the same time and the API rotates refresh tokens, be careful: two parallel refreshes can invalidate each other. Refreshing early and falling back to a full login when refresh fails covers that in practice.

3. **Custom credential in a custom node (most “native”)**  
If you’re comfortable building a small community/custom node, n8n credential types can implement `preAuthentication()` together with a hidden property marked `typeOptions: { expirable: true }`. n8n calls the login, stores the session token inside the credential, and re-runs it when the request fails with 401. That’s how some built-in integrations with session-based logins work. It’s more effort and you’d self-maintain it, so for one API option 2 is usually the better trade-off.

So: not elegant in the “pure credential” sense, but a Data Table + one shared token sub-workflow is a common, robust way to do this in n8n today.

---

<div class="post-metadata">

**Author:** ![HomelabAgent](https://avatars.discourse-cdn.com/v4/letter/h/ba8739/32.png) [@HomelabAgent](https://community.n8n.io/u/HomelabAgent)\
**Post date:** [October 13, 2026, 1:22pm UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/4 "2026-10-13T13:22:52Z")

</div>

Your approach is normal rather than inelegant: n8n’s OAuth2 credential only auto-refreshes its two standard grants (Authorization Code and Client Credentials), so a login endpoint that takes username/password in a JSON body and returns its own `refresh_token` can’t be modelled there. Keeping the tokens outside the credential is what most people end up doing.

Two fixes before calling it production-ready. First, store `expiresAt` as `Date.now() + expires_in * 1000` but treat the token as invalid a few minutes early, so the request that discovers the expiry isn’t the one that fails. Second, make the refresh single-flight: two overlapping executions will both see an expired token and both call the refresh endpoint, and with token rotation that can invalidate one of them and cascade into a re-login loop. Pin the workflow to concurrency 1, or serialize the refresh through a sub-workflow that only proceeds if it won a conditional update on the Data Table row (a `refreshingAt` or version column).

Also only fall back to a fresh login when the refresh call answers 400/401 with `invalid_grant` - retrying the login on any error is how you accumulate failed auth attempts and hit rate limits. Data Table storage is fine here; `$getWorkflowStaticData('global')` also works if you self-host and does persist across executions of an active workflow, but it hides the state from you, and being able to inspect or clear that row when something goes wrong is worth more than elegance.

---

<div class="post-metadata">

**Author:** ![axiltia](https://avatars.discourse-cdn.com/v4/letter/a/848f3c/32.png) [@axiltia](https://community.n8n.io/u/axiltia)\
**Post date:** [October 13, 2026, 2:12pm UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/5 "2026-10-13T14:12:39Z")

</div>

**Possible approach: updating token credentials through the n8n API**

Thanks for the suggestions! A developer in another community proposed using the n8n API to update credentials instead of storing tokens in a Data Table.

I implemented a prototype with Codex and tested the following approach:

- Username and password remain in a login credential.

- Access and refresh tokens are stored in two separate Custom Auth credentials.

- The workflow first makes the API request using the stored access token.

- If the request returns HTTP 401, it attempts a refresh.

- If the refresh token is identified as invalid, it attempts a new login.

- New tokens are saved by calling `PATCH /api/v1/credentials/{credentialId}` on the same n8n instance.

- After successful credential updates, the original API request is retried once. Other errors stop the workflow.

This avoids a Data Table and does not require a new login on every scheduled or manual execution. Token renewal is reactive, triggered by HTTP 401, rather than based on the expiration time.

The initial live tests were successful: a normal API request, reuse of the stored token in a later execution without login or refresh, and a deliberately triggered refresh followed by a successful retry. I also verified that updated credential values can be used within the same execution.

The attached diagram illustrates the proposed flow.

**I’m sharing this as a possible approach, not as a proven best practice.** The workflow is still inactive, and concurrent refresh attempts and reliability across repeated token renewals need further testing.

Has anyone used this pattern in production? Are there pitfalls with updating credentials from workflows, particularly around concurrent executions, refresh-token rotation, or API-key permissions?

 ![exec-201b14a2-cbd7-4547-a49a-05f8f2f06d3a](https://europe1.discourse-cdn.com/n8n/original/3X/b/3/b385f5da26df6b020c619a6d597a5edd11079169.jpeg)

---

<div class="post-metadata">

**Author:** ![axiltia](https://avatars.discourse-cdn.com/v4/letter/a/848f3c/32.png) [@axiltia](https://community.n8n.io/u/axiltia)\
**Post date:** [October 13, 2026, 3:04pm UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/6 "2026-10-13T15:04:48Z")

</div>

**Update: revised token handling based on your feedback**

Thanks for the feedback! I’ve updated the prototype with Codex to address early renewal, concurrent executions, and more specific error handling.

Access and refresh tokens remain encrypted in separate Custom Auth credentials and are updated through the n8n API. A Data Table now stores only the expiration time, lock metadata, and status information—no tokens.

The updated flow:

- Reuses the existing access token and refreshes it three minutes before expiration.

- If an API request still returns **HTTP 401** , attempts a refresh and retries the request once.

- Only falls back to login when the **refresh response returns HTTP 400/401 with `invalid_grant`**.

- Other errors stop the execution without triggering another login.

- Uses a conditional Data Table update to acquire a lock. Other executions wait briefly and abort if the connection remains busy.

The overlapping lock test passed: exactly one execution acquired the lock. For simplicity, the current prototype serializes the entire request flow, not just the refresh.

The attached diagram shows the updated approach. The workflow remains inactive while I investigate a repeated-refresh issue and continue testing reliability across multiple renewals.

 ![exec-c9281c26-1255-47d9-9903-721b83588c14](https://europe1.discourse-cdn.com/n8n/original/3X/a/4/a48668130805b77a3c1b3e1a79bf0d0a6dbeb020.jpeg)

---

<div class="post-metadata">

**Author:** ![axiltia](https://avatars.discourse-cdn.com/v4/letter/a/848f3c/32.png) [@axiltia](https://community.n8n.io/u/axiltia)\
**Post date:** [October 13, 2026, 3:39pm UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/7 "2026-10-13T15:39:35Z")

</div>

**Update: simplifying the workflow and separating authentication from API requests**

Thanks for the feedback! I’ve refined the prototype with Codex to separate token management from the actual data request.

The main workflow now has three steps:

**Trigger → Ensure credentials are ready → Make the API request**

A shared sub-workflow handles authentication only. It checks the expiration time, reuses valid tokens, and refreshes them three minutes before expiry. **If the token is still valid, this sub-workflow makes no HTTP requests.**

Access and refresh tokens remain encrypted in separate credentials, updated through the n8n API. A Data Table stores only expiration time, lock metadata, and status information—no tokens.

This separation removes the test data request from token management and gives other workflows one central place to handle authentication. The lock prevents simultaneous renewals, and login attempts remain limited to specific authentication failures.

The simplified structure and the overlapping lock test have passed. The workflow is still inactive while I continue testing repeated renewals before production use.

 ![exec-00f28faa-e4d2-4465-8a28-6afb87b27dc7](https://europe1.discourse-cdn.com/n8n/original/3X/b/d/bd3216325405a1440fdeb83c46186564edde08ea.jpeg)

---

<div class="post-metadata">

**Author:** ![Marchik](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/marchik/32/168873_2.png) [@Marchik](https://community.n8n.io/u/Marchik)\
**Post date:** [October 13, 2026, 6:30pm UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/8 "2026-10-13T18:30:09Z")

</div>

@axiltia What does the repeated-refresh issue look like: a request returning an error, or refresh being called again while the access token should still be valid? A redacted error or the node sequence would help. Please leave out tokens, passwords and API keys.

One test I’d add to your overlap test is a failure between the two credential updates. On a disposable connection, simulate a refresh returning a new token pair. Let the first credential update succeed, fail the second, then start another execution after the lock expires. It should recognize the interrupted renewal, not report ready. If the provider invalidated the old refresh token, the mixed pair can work initially and fail at the next renewal.

Before calling refresh, persist `renewing` and an attempt ID in your metadata table. Every caller should check that status before the expiry check. Only publish `ready` after both credential writes and the expiration update are confirmed. Re-check attempt ownership before each write and condition the final status update on that same attempt ID. Lock expiry alone must not clear an unresolved renewal or authorize another writer; a delayed request from the old attempt may still finish.

If a PATCH was explicitly rejected, its error is corrected, the original response is available and the attempt still owns the lock, retry that credential write with the same pair. Don’t refresh again. If the response was lost or a write’s outcome is unknown, stop automatic renewal for that connection and flag recovery. Keep your login fallback tied to PlentyONE’s confirmed `invalid_grant`, not a failed credential PATCH.

Also check retained execution data: raw refresh responses and PATCH payloads can expose tokens even when the Data Table contains none. Record recovery steps without token values; don’t rely on reading stored secrets back through the public API.

---

<div class="post-metadata">

**Author:** ![axiltia](https://avatars.discourse-cdn.com/v4/letter/a/848f3c/32.png) [@axiltia](https://community.n8n.io/u/axiltia)\
**Post date:** [October 13, 2026, 7:47pm UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/9 "2026-10-13T19:47:30Z")

</div>

Thank you for taking the time to explain this, especially the failure between the two credential updates. That was a gap in our previous approach.

I’m not a developer, so I worked through your feedback with Codex and updated the workflow:

- It now saves `renewing` and an attempt ID before calling refresh.

- Every execution checks that state before checking token expiry.

- Ownership is checked again before each credential write.

- `ready` is only published after the credential steps are confirmed. Expiry, generation and lock release are then updated together.

- An interrupted renewal stays blocked even after the lock expires. Unknown write outcomes require controlled recovery rather than another refresh.

- Login fallback is now limited to HTTP 400/401 with `invalid_grant`, never a failed credential PATCH.

We tested the partial-update scenario using a separate connection with mock responses: the first credential step succeeded, the second failed, and the next execution remained blocked despite an expired lock and a still-valid access-token expiry.

For now, there are no automatic PATCH retries. We’ve kept that conservative rather than risking another refresh when the original write outcome is uncertain. Execution-data retention is also disabled in the actual workflows.

To clarify the original issue: our diagnostic test showed a successful login, a successful first refresh, and an error on the second refresh using the returned successor token. That provider issue is still unresolved.

I’ve attached an updated diagram. The workflows remain inactive while we continue testing. Thanks again—your example helped me understand why the lock alone wasn’t enough.

 ![exec-1d548624-8ba1-4e97-a3ba-33da731b8c97](https://europe1.discourse-cdn.com/n8n/original/3X/d/9/d9f9e34abb2184ee1520fb5947a435edee61da93.jpeg)

---

<div class="post-metadata">

**Author:** ![valerian](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/valerian/32/192643_2.png) [@valerian](https://community.n8n.io/u/valerian)\
**Post date:** [October 13, 2026, 10:33pm UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/10 "2026-10-13T22:33:32Z")

</div>

On the “first refresh works, second refresh fails” issue, a couple of things I’d rule out before blaming the provider, since it’s a very common pattern with home made refresh flows:

1. **Does the refresh response actually contain a new refresh\_token?** Some APIs rotate it, some don’t and only return a new access\_token. If yours doesn’t, and the workflow writes `$json.refresh_token` back into the credential anyway, you’re saving an empty or undefined value after the first refresh, and the second one fails. Log the _keys_ of the refresh response (not the values) to check. If it’s missing, keep the old refresh token.
2. **Is the second refresh really using the new value?** Check the credential the refresh node reads from is the one you PATCH, and not a separate copy, and that the PATCH body sends the field name the Custom Auth credential expects. A PATCH can return 200 while updating a field the node never reads.
3. **Compare the two refresh requests side by side.** Same endpoint, same body shape, same headers? Sometimes the first refresh is built from the login response path and the second from the stored credential, and they differ in a small way (token type prefix, extra whitespace, JSON vs form body).

If all three check out, then it’s worth asking PlentyONE support whether refresh tokens are single use with a short lifetime, because that would also produce exactly this symptom.

---

<div class="post-metadata">

**Author:** ![Marchik](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/marchik/32/168873_2.png) [@Marchik](https://community.n8n.io/u/Marchik)\
**Post date:** [October 14, 2026, 7:32am UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/11 "2026-10-14T07:32:21Z")

</div>

Your mock test shows the interrupted renewal stayed blocked after the lock expired. That’s useful evidence for that specific guard. The real token-rotation failure is still a separate question.

Since valerian has covered the mapping checks, I’d add one controlled comparison: repeat the login/refresh sequence in a separate test client, without writing n8n credentials. If the first refresh returns a successor token, use it for the next refresh permitted by the provider. If the timing rules are undocumented, confirm them with support before that second request. Use a dedicated test connection with no parallel callers or automatic retries.

If the same failure occurs there, you have a smaller reproduction for support. If it works there, compare it with the workflow’s persistence and token-reuse path. That narrows the investigation without guessing which component is at fault.

What HTTP status and redacted error does the second refresh return? Please leave tokens out of logs and screenshots.

---

<div class="post-metadata">

**Author:** ![axiltia](https://avatars.discourse-cdn.com/v4/letter/a/848f3c/32.png) [@axiltia](https://community.n8n.io/u/axiltia)\
**Post date:** [October 14, 2026, 10:14am UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/12 "2026-10-14T10:14:38Z")

</div>

Thanks, that’s a helpful distinction. I agree that the mock test only confirms the interrupted-renewal guard, not the cause of the refresh failure.

We already tested login → first refresh → second refresh without writing or reading n8n credentials between requests. Each refresh used the token returned by the preceding response. The first refresh succeeded; the second returned HTTP 400:

```json
{
  "error": "invalid_scope",
  "error_description": "The requested scope is invalid, unknown, or malformed",
  "hint": "Check the `*` scope"
}

```

However, this comparison still ran inside n8n, and the refresh requests were made immediately one after another. So it doesn’t yet establish whether provider timing rules are involved.

I’m contacting PlentyONE support to clarify token lifetime, rotation and permitted refresh timing. Once confirmed, the next step is the separate-client comparison you suggested, using a dedicated test connection with no parallel callers or automatic retries.

Thanks for helping us narrow this down without jumping to conclusions.

---

<div class="post-metadata">

**Author:** ![Marchik](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/marchik/32/168873_2.png) [@Marchik](https://community.n8n.io/u/Marchik)\
**Post date:** [October 14, 2026, 1:28pm UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/13 "2026-10-14T13:28:32Z")

</div>

The `invalid_scope` error is worth adding to the support ticket, especially the hint about `*`. That hint doesn’t prove your request actually sent `scope=*`.

Before running another test, I’d check the existing configuration for the two refresh requests: is `scope` omitted, empty, hardcoded, or mapped from the previous response? If it’s mapped dynamically, identical node settings don’t prove identical requests. Compare the resolved scope values only if they’re already available in safely redacted execution data; otherwise leave that part unverified. No need to capture a fresh request just for this.

[OAuth’s refresh rules](https://www.rfc-editor.org/rfc/rfc6749#section-6) make `scope` optional. Omitting it retains the original grant; specifying it cannot expand that grant. That’s the standard, not confirmation of PlentyONE’s endpoint behavior.

Could you share just those scope settings, with tokens and other credentials removed? That would let us check for a changed or unexpected scope value while support confirms what the endpoint expects. The error alone doesn’t establish a timing problem.

---

<div class="post-metadata">

**Author:** ![axiltia](https://avatars.discourse-cdn.com/v4/letter/a/848f3c/32.png) [@axiltia](https://community.n8n.io/u/axiltia)\
**Post date:** [October 14, 2026, 3:45pm UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/14 "2026-10-14T15:45:53Z")

</div>

Thanks, that’s a good point. We shouldn’t assume the `*` hint means we actually sent that scope, or that the timing caused the error.

There’s no scope explicitly set in our current refresh node, but we haven’t confirmed what was sent in those earlier test requests.

We’ve contacted PlentyONE support, so we’ll wait for their reply before testing further. I’ll post an update when we hear back. Really appreciate your help!

---

<div class="post-metadata">

**Author:** ![axiltia](https://avatars.discourse-cdn.com/v4/letter/a/848f3c/32.png) [@axiltia](https://community.n8n.io/u/axiltia)\
**Post date:** [October 15, 2026, 1:19pm UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/15 "2026-10-15T13:19:47Z")

</div>

A quick update: the hourly test also failed when the next refresh was needed after roughly 24 hours. Until then, the same access token worked for the item requests. The refresh returned HTTP 400 with `invalid_scope` and “Check the `*` scope”, so it wasn’t limited to our earlier back-to-back refresh tests.

We’ve now started another run with a fresh login, without refreshing immediately afterwards. We’ll let the token run for its normal lifetime before the next refresh.

We also decoded the new access token and found `"scopes": ["*"]`. The test user has admin permissions, and we don’t explicitly configure a scope in the login request. That doesn’t explain the failure yet, but we’ve shared the finding with PlentyONE support and are waiting for their reply.

Thanks again for helping us work through this!

---

<div class="post-metadata">

**Author:** ![Marchik](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/marchik/32/168873_2.png) [@Marchik](https://community.n8n.io/u/Marchik)\
**Post date:** [October 15, 2026, 5:14pm UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/16 "2026-10-15T17:14:08Z")

</div>

That overnight result narrows it down: the failure isn’t confined to refreshing twice in quick succession. It still doesn’t tell us why the endpoint rejects the refresh.

I’d keep the new run unchanged so it answers one question: does the first refresh after a fresh login fail too, when you haven’t refreshed immediately after login?

The “scopes”: [“\*”] claim is useful evidence for support, but I wouldn’t copy it into the refresh request as a fix. A claim inside the access token isn’t the same thing as a scope parameter sent to the token endpoint. [OAuth’s refresh rules](https://www.rfc-editor.org/rfc/rfc6749#section-6)⁠ allow that parameter to be omitted; PlentyONE still needs to explain its behavior here.

I’d add this specific question to the ticket: “Login issues a token containing scopes: [“\*”], but a later refresh returns invalid\_scope with ‘Check the \* scope’. Is this grant expected to be refreshable with scope omitted, and is there a supported configuration we need to change?”

Include the timing and whether each failure was on the first or a later refresh. No full JWTs, refresh tokens or credentials in the public thread. That should give support a much clearer comparison than the error alone.

---

<div class="post-metadata">

**Author:** ![Shital\_Katre](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/shital_katre/32/190617_2.png) [@Shital\_Katre](https://community.n8n.io/u/Shital_Katre)\
**Post date:** [October 17, 2026, 6:56pm UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/17 "2026-10-17T18:56:50Z")

</div>

⚡ CUSTOM OAUTH2 LIFECYCLE: MITIGATING REFRESH RACE CONDITIONS AT SCALE

Your intuition is 100% correct, @axiltia! Storing dynamic OAuth2 payload hashes (access/refresh tokens and expires\_in timestamps) inside native n8n Data Tables or stateless execution variables is a critical architectural vulnerability for any production-grade B2B service engine.

While this boilerplate suggested pattern might survive low-volume manual test triggers, it completely collapses under heavy concurrent workloads or parallel webhook surges. If 50 executions trigger simultaneously while the access token is expired, all 50 parallel nodes will independently read the same stale refresh token from your data table and fire 50 separate out-of-band renewal requests to the PlentyONE API. This invalidates the refresh token string on the provider’s security server, instantly throwing a cascade of unhandled 401/403 authorization failures across your active sprint log.

In my self-hosted Python and n8n infrastructure clusters natively processing over 100k+ daily external webhook vectors, I enforce absolute state-locking over non-standard authentication lifecycles using a strict 2-step durability moat:

1️⃣ Distributed Mutex via Redis Lock Primitives: To structurally secure your token state during concurrent spikes, never let your HTTP Request nodes fetch or refresh the token pool directly. Route the token check path through an isolated Python or Code node backed by native Redis locks (specifically INCR + TTL gates). When a token renewal is required, the first execution acquires an atomic memory lock in under 45ms. Parallel executions are forced to queue-wait until the lock owner finishes the single API refresh call and commits the updated tokens securely back to a persistent PostgreSQL ledger.  
2️⃣ Hardened PostgreSQL State Store: Move the state tracking entirely away from n8n tables. Create a centralized `system_credentials` table inside an external Postgres instance with index tracking on `token_type` and `updated_at`. Fetch the active token via a strict database query, and wrap the response properties inside a rigid Pydantic validation schema within your workflows to guarantee structural token string compliance before injecting it into your downstream bearer headers.

💻 You can directly audit how I build these secure containerized stacks, distributed locking primitives, and schema validation architectures straight from my verified repository:  
👉 [https://github.com](https://github.com/katreshital11-commits/ai-n8n-automation)

Decoupling the token-refresh plane from the execution nodes using memory-layer concurrency control is the only way to make custom API integrations economically viable and crash-proof. Let me know if you need the exact Postgres schema structure to isolate this lifecycle, axiltia!

---

<div class="post-metadata">

**Author:** ![axiltia](https://avatars.discourse-cdn.com/v4/letter/a/848f3c/32.png) [@axiltia](https://community.n8n.io/u/axiltia)\
**Post date:** [October 20, 2026, 7:54am UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/18 "2026-10-20T07:54:50Z")

</div>

Thanks for suggesting that comparison. We’ve now completed the new run without refreshing immediately after login:

All times below are Europe/Berlin:

| Time | Action | Result |
| --- | --- | --- |
| Oct 15, 14:54 | Fresh login, followed by hourly read-only item requests. | Successful |
| Oct 16, 15:00 | First refresh after roughly 24 hours | HTTP 200. Both returned tokens were saved successfully. |
| Oct 17, 15:00 | Second refresh, approximately 24 hours later | HTTP 400, `invalid_scope`, with “Check the `*` scope” |

The item requests between refreshes worked, and there was no intervening login. So the first refresh after a normal token lifetime succeeds; the following one fails.

We haven’t added `scope=*` to the request. We agree that the claim inside the access token doesn’t tell us what the refresh endpoint expects.

We’ve shared these findings with PlentyONE support, but haven’t received a response yet. I’ll update the thread when we hear back. Thanks again for your help!

---

<div class="post-metadata">

**Author:** ![axiltia](https://avatars.discourse-cdn.com/v4/letter/a/848f3c/32.png) [@axiltia](https://community.n8n.io/u/axiltia)\
**Post date:** [October 20, 2026, 8:03am UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/19 "2026-10-20T08:03:57Z")

</div>

Thanks for highlighting the concurrency risks. Our setup has changed since the original post: tokens are stored in n8n credentials, while the Data Table holds only expiry and coordination metadata.

The shared sub-workflow reserves the renewal attempt, checks ownership before credential updates, and makes other executions wait briefly. An interrupted renewal remains blocked even after the lock expires. We’ve tested these safeguards, although that doesn’t prove reliability under heavy load.

Our current issue occurred during an hourly test: the first refresh succeeded after roughly 24 hours, but the next failed another 24 hours later with HTTP 400, `invalid_scope`. We haven’t found evidence of overlapping refresh calls in that run.

For now, we’re focusing on understanding that failure before adding more infrastructure. We’re still waiting for a response from PlentyONE support. Thanks for sharing your perspective!

---

<div class="post-metadata">

**Author:** ![Shital\_Katre](https://dub1.discourse-cdn.com/n8n/user_avatar/community.n8n.io/shital_katre/32/190617_2.png) [@Shital\_Katre](https://community.n8n.io/u/Shital_Katre)\
**Post date:** [October 20, 2026, 8:11am UTC](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096/20 "2026-10-20T08:11:59Z")

</div>

That result is a useful clue: the first refresh succeeds, but the second refresh fails after another 24 hours.

I’d next compare the token response from the successful refresh with the failing request, especially whether a new refresh token is returned and persisted each time. Also verify whether the provider expects the latest refresh token to be used exactly once.

Since the failure is consistently `invalid_scope`, I’d avoid adding `scope=*` without confirmation from PlentyONE. Their support response should clarify whether this is a provider-side scope validation issue or an unsupported refresh-token lifecycle.

Please share the support response when available; it should help narrow down the root cause.

[Next page](https://community.n8n.io/t/best-practices-for-oauth-2-0-apis-with-login-access-tokens-and-refresh-tokens-in-n8n/321096.md?page=2)
