Credential Test button returns 401 Unauthorized, while node & Postman succeed
Describe the problem/error/question
I’m implementing a custom credential for NetSuite OAuth 1.0 (HMAC-SHA256).
-
Custom node executions succeed using the same signer and credentials.
-
Postman requests succeed against the same endpoint and creds.
-
The Credentials “Test” button in n8n always fails with 401 Unauthorized.
It looks like the signature produced for the Credential Test request (via preAuthentication) isn’t being accepted by NetSuite, even though I sign the exact same URL and method that the Test request uses.
I want to confirm whether the Credential Test request path/encoding/headers are altered in a way that changes the OAuth 1.0 signature base string, or if there’s a known issue with passing a precomputed Authorization header from preAuthentication to test.request.headers.
What is the error message (if any)?
Couldn't connect with these settings
Unauthorized
Please share your workflow
Not directly applicable (the issue is isolated to the Credential Test), but the same credentials work inside my custom node.
Share the output returned by the last node
Example successful response (from the custom node calling SuiteQL)
Code
credentials/NetSuiteOAuth1Api.credentials.ts
/******************************************************************
* credentials/NetSuiteOAuth1Api.credentials.ts
******************************************************************/
import {
ICredentialTestRequest,
ICredentialType,
INodeProperties,
IHttpRequestHelper,
ICredentialDataDecryptedObject,
LoggerProxy as Logger,
} from 'n8n-workflow';
import { generateNetSuiteAuth } from '../nodes/Netsuite/NetsuiteAuth';
export class NetSuiteOAuth1Api implements ICredentialType {
name = 'netSuiteOAuth1Api';
displayName = 'NetSuite OAuth-1 API';
documentationUrl = 'https://docs.oracle.com';
description = { icon: 'file:netsuite.svg' };
properties: INodeProperties[] = [
{ displayName: 'Consumer Key', name: 'consumerKey', type: 'string', typeOptions: { password: true }, required: true, default: '' },
{ displayName: 'Consumer Secret', name: 'consumerSecret', type: 'string', typeOptions: { password: true }, required: true, default: '' },
{ displayName: 'Access Token', name: 'accessToken', type: 'string', typeOptions: { password: true }, required: true, default: '' },
{ displayName: 'Token Secret', name: 'tokenSecret', type: 'string', typeOptions: { password: true }, required: true, default: '' },
{ displayName: 'Account ID', name: 'accountId', type: 'string', required: true, default: '', description: 'e.g. 30000-SB1' },
{ displayName: 'Realm', name: 'realm', type: 'string', required: true, default: '', description: 'e.g. 30000_SB1' },
{
displayName: 'Signature Method',
name: 'signatureMethod',
type: 'options',
options: [{ name: 'HMAC-SHA256', value: 'HMAC-SHA256' }],
required: true,
default: 'HMAC-SHA256',
},
{
displayName: 'Licence Key',
name: 'licenseKey',
type: 'string',
typeOptions: { password: true },
required: true,
default: '',
description: 'Licence key provided by MRK',
},
];
// Credential Test – calls metadata-catalog (same endpoint that works in Postman)
test: ICredentialTestRequest = {
request: {
baseURL: '={{`https://${$credentials.accountId}.suitetalk.api.netsuite.com`}}',
url: '/services/rest/record/v1/metadata-catalog',
method: 'GET',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: '={{$credentials.authHeader}}',
},
},
};
// preAuthentication signs the exact URL/method that the Test request will send
async preAuthentication(
this: IHttpRequestHelper,
credentials: ICredentialDataDecryptedObject,
) {
const method = 'GET';
const baseURL = `https://${String(credentials.accountId)}.suitetalk.api.netsuite.com`;
const path = '/services/rest/record/v1/metadata-catalog';
const url = `${baseURL}${path}`;
const { Authorization } = generateNetSuiteAuth(
method,
url,
String(credentials.consumerKey),
String(credentials.consumerSecret),
String(credentials.accessToken),
String(credentials.tokenSecret),
String(credentials.realm),
{}, // no query params
);
Logger.debug('Generated OAuth1 header for Test request');
return { authHeader: Authorization }; // plain string used by Test header
}
}
nodes/Netsuite/NetsuiteAuth.ts (helper)
import crypto from 'crypto';
export function generateNetSuiteAuth(
method: string,
url: string,
consumerKey: string,
consumerSecret: string,
token: string,
tokenSecret: string,
realm: string,
additionalParams: Record<string, string> = {},
): { Authorization: string } {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString('hex');
const oauthParams: Record<string, string> = {
oauth_consumer_key: consumerKey,
oauth_token: token,
oauth_signature_method: 'HMAC-SHA256',
oauth_timestamp: timestamp,
oauth_nonce: nonce,
oauth_version: '1.0',
...additionalParams,
};
const sortedParams = Object.keys(oauthParams)
.sort()
.reduce((acc: Record<string, string>, key) => {
acc[key] = oauthParams[key];
return acc;
}, {});
let signatureBase = method.toUpperCase() + '&' + encodeURIComponent(url) + '&';
const paramString = Object.entries(sortedParams)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join('&');
signatureBase += encodeURIComponent(paramString);
const signingKey = `${encodeURIComponent(consumerSecret)}&${encodeURIComponent(tokenSecret)}`;
const signature = crypto.createHmac('sha256', signingKey).update(signatureBase).digest('base64');
const authHeader =
`OAuth realm="${realm}",` +
`oauth_consumer_key="${oauthParams.oauth_consumer_key}",` +
`oauth_token="${oauthParams.oauth_token}",` +
`oauth_signature_method="HMAC-SHA256",` +
`oauth_timestamp="${oauthParams.oauth_timestamp}",` +
`oauth_nonce="${oauthParams.oauth_nonce}",` +
`oauth_version="1.0",` +
`oauth_signature="${encodeURIComponent(signature)}"`;
return { Authorization: authHeader };
}
Information on your n8n setup
-
n8n version: 1.93.0
-
Database: postgres
-
n8n EXECUTIONS_PROCESS:
own -
Running n8n via: Self-hosted Docker (
n8nio/n8n:1.93.0) -
Operating system (host): Ubuntu 22.04 LTS
Questions for the n8n team
-
In credential tests, is any transformation applied to
Authorization(or the request URL) afterpreAuthenticationreturns it, which could alter the OAuth base string? -
Are there known issues in 1.93.0 where the Credential Test flow handles
preAuthenticationoutputs differently from node execution? -
Is there a recommended pattern to ensure OAuth1 HMAC-SHA256 signatures generated in
preAuthenticationare sent verbatim by the Test request?
Thanks!