I’ve done this exact thing before. achamm’s recommendation of n8n-nodes-azure-openai-ms-oauth2 is the cleanest reference if you’re building a custom node — it manages the full token lifecycle in the node’s own preSend hook and adds the resource body parameter on refresh exactly like you need. The source is on npm and GitHub, easy to fork.
If you want an even simpler starter template to look at, the built-in n8n Google Drive node has custom OAuth2 logic in its credential definition that handles token refresh with extra parameters (it’s in the n8n core repo under packages/nodes-base/credentials/GoogleOAuth2Api.credentials.ts). The pattern is nearly identical: store tokens, check expiry, POST to the token endpoint with whatever extra body fields you need, update the access token before the real request goes out.
For your specific case — custom OAuth server, need resource in the body — the preSend logic would look something like this (inside your custom node’s execute or preSend method):
async preSend(request, options) {
const credentials = await this.getCredentials(‘myCustomOAuth2Api’);
// Check if token is expired
if (Date.now() > credentials.expiresAt) {
const refreshParams = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: credentials.refreshToken,
client_id: credentials.clientId,
client_secret: credentials.clientSecret,
resource: credentials.resource, // the crucial body param
});
const tokenResponse = await this.helpers.httpRequest({
method: 'POST',
url: credentials.accessTokenUrl,
body: refreshParams.toString(),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
});
// Update credentials cache
credentials.accessToken = tokenResponse.access_token;
credentials.refreshToken = tokenResponse.refresh_token;
credentials.expiresAt = Date.now() + (tokenResponse.expires_in \* 1000);
await this.setCredentials('myCustomOAuth2Api', credentials);
}
// Attach access token to original request
request.headers.Authorization = `Bearer ${credentials.accessToken}`;
return request;
}
That’s essentially the blueprint. For production, you’d add error handling and token storage properly (n8n’s credentials object persists via the database automatically).
But before you write a single line of code — try the query-param trick first. Just append ?resource=https://your-target to your accessTokenUrl. If the server accepts it as a query param, you’re done in 10 seconds. Many custom OAuth servers do, because they parse both. If it’s strict body-only, then the custom node route is solid and maintainable long-term.
Let me know which path you end up taking — happy to help debug the preSend hook if you get stuck.