n8n-nodes-refresh-token-auth
Version:
n8n community node with automatic refresh token support
331 lines • 13.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.DebugRefreshToken = void 0;
const CREDENTIALS_NAME = 'refreshTokenAuth';
const SENSITIVE_HEADERS = [
'authorization',
'x-api-key',
'api-key',
'client-secret',
'client_secret',
];
const SENSITIVE_BODY_KEYS = ['access_token', 'refresh_token', 'client_secret', 'id_token'];
const isObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
function safeParseBody(body) {
if (Buffer.isBuffer(body))
body = body.toString('utf8');
if (typeof body === 'string') {
try {
return JSON.parse(body);
}
catch {
return body;
}
}
return body !== null && body !== void 0 ? body : null;
}
function redactHeaders(h, extraSensitiveKeys = []) {
const out = {};
const allSensitiveKeys = [
...SENSITIVE_HEADERS,
...extraSensitiveKeys.filter((k) => !k.includes('.')),
];
for (const [k, v] of Object.entries(h || {})) {
if (allSensitiveKeys.some((sk) => sk.toLowerCase() === k.toLowerCase())) {
out[k] = '***REDACTED***';
}
else if (typeof v === 'string' && v.trim().startsWith('eyJ')) {
out[k] = '***REDACTED***';
}
else {
out[k] = v;
}
}
return out;
}
function redactAtPath(obj, path) {
if (!obj || typeof obj !== 'object')
return;
const parts = path.split('.');
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
const actualKey = Object.keys(current).find((k) => k.toLowerCase() === part.toLowerCase());
if (!actualKey || !current[actualKey] || typeof current[actualKey] !== 'object')
return;
current = current[actualKey];
}
const lastPart = parts[parts.length - 1];
const actualKey = Object.keys(current).find((k) => k.toLowerCase() === lastPart.toLowerCase());
if (actualKey && current[actualKey] !== undefined) {
current[actualKey] = '***REDACTED***';
}
}
function redactBodyDeep(v, extraSensitiveKeys = []) {
if (!v || typeof v !== 'object')
return v;
const copy = JSON.parse(JSON.stringify(v));
const simpleSensitiveKeys = [...SENSITIVE_BODY_KEYS];
const pathSensitiveKeys = [];
for (const key of extraSensitiveKeys) {
if (key.includes('.')) {
pathSensitiveKeys.push(key);
}
else {
simpleSensitiveKeys.push(key);
}
}
const walk = (o) => {
if (!o || typeof o !== 'object')
return;
for (const k of Object.keys(o)) {
const value = o[k];
if (simpleSensitiveKeys.some((sk) => sk.toLowerCase() === k.toLowerCase())) {
o[k] = '***REDACTED***';
}
else if (typeof value === 'string' && value.trim().startsWith('eyJ')) {
o[k] = '***REDACTED***';
}
else {
walk(value);
}
}
};
walk(copy);
for (const path of pathSensitiveKeys) {
redactAtPath(copy, path);
}
return copy;
}
function looksLikePreAuth(url, body, tokenUrlFromCreds) {
const u = (url || '').toLowerCase();
if (tokenUrlFromCreds && u === tokenUrlFromCreds.toLowerCase())
return true;
const asStr = typeof body === 'string' ? body : Buffer.isBuffer(body) ? body.toString('utf8') : '';
if (asStr.includes('grant_type=refresh_token'))
return true;
if (isObject(body) && String(body.grant_type) === 'refresh_token')
return true;
if (u.includes('/oauth') && u.includes('/token'))
return true;
return false;
}
function truncate(v, maxLen) {
if (maxLen === 0)
return v;
const s = typeof v === 'string' ? v : JSON.stringify(v, null, 2);
if (s.length > maxLen) {
return s.slice(0, maxLen) + `… [truncated ${s.length - maxLen} chars]`;
}
return v;
}
function formatEvent(e, redact, truncLen, extraSensitiveKeys = []) {
return {
...e,
headers: redact ? redactHeaders(e.headers, extraSensitiveKeys) : e.headers,
body: truncate(redact ? redactBodyDeep(e.body, extraSensitiveKeys) : e.body, truncLen),
};
}
class DebugRefreshToken {
constructor() {
this.description = {
displayName: 'RefreshToken (Debug)',
name: 'refreshToken',
subtitle: '={{$parameter["method"] + ": " + $parameter["url"]}}',
icon: 'file:refresh-token-auth.svg',
group: ['transform'],
version: 1,
description: 'Runs an authenticated request with RefreshToken credentials and captures BOTH the token refresh and the main call (headers + bodies).',
defaults: { name: 'RefreshToken (Debug)' },
inputs: ['main'],
outputs: ['main'],
credentials: [{ name: CREDENTIALS_NAME, required: true }],
properties: [
{
displayName: 'URL',
name: 'url',
type: 'string',
default: '',
required: true,
placeholder: 'https://api.example.com/v1/resource',
},
{
displayName: 'Method',
name: 'method',
type: 'options',
default: 'GET',
options: ['DELETE', 'GET', 'PATCH', 'POST', 'PUT'].map((m) => ({ name: m, value: m })),
},
{
displayName: 'Query (JSON)',
name: 'qsJson',
type: 'json',
default: '{}',
description: 'Merged into query string of the main request',
},
{
displayName: 'Headers (JSON)',
name: 'headersJson',
type: 'json',
default: '{}',
description: 'Extra headers for the main request',
},
{
displayName: 'Body (JSON)',
name: 'bodyJson',
type: 'json',
default: '{}',
description: 'Used for POST/PUT/PATCH/DELETE when sending JSON',
displayOptions: { show: { method: ['POST', 'PUT', 'PATCH', 'DELETE'] } },
},
{
displayName: 'Send Pre-Authentication',
name: 'sendPreauth',
type: 'boolean',
default: true,
description: 'Whether to send pre-auth requests. If off, pre-auth requests are captured but NOT sent (mocked response).',
},
{
displayName: 'Send Main Request',
name: 'sendMain',
type: 'boolean',
default: true,
description: 'Whether to send main request. If off, main request is captured but NOT sent (mocked response).',
},
{
displayName: 'Redact Secrets in Output',
name: 'redact',
type: 'boolean',
default: false,
description: 'Whether to mask Authorization, tokens, and client_secret',
},
{
displayName: 'Truncate Bodies To (Chars)',
name: 'truncate',
type: 'number',
typeOptions: { minValue: 0 },
default: 10000,
description: '0 = no truncation',
},
],
};
}
async execute() {
var _a, _b, _c;
this.logger.debug('🔥🔥🔥 RefreshToken.execute CALLED! 🔥🔥🔥');
const items = this.getInputData();
const out = [];
let tokenUrlFromCreds;
let accessTokenFieldName;
let refreshTokenFieldName;
try {
const creds = (await this.getCredentials(CREDENTIALS_NAME));
tokenUrlFromCreds = ((_c = (_b = (_a = creds === null || creds === void 0 ? void 0 : creds.tokenUrl) !== null && _a !== void 0 ? _a : creds === null || creds === void 0 ? void 0 : creds.accessTokenUrl) !== null && _b !== void 0 ? _b : creds === null || creds === void 0 ? void 0 : creds.refreshUrl) !== null && _c !== void 0 ? _c : creds === null || creds === void 0 ? void 0 : creds.authUrl);
accessTokenFieldName = creds === null || creds === void 0 ? void 0 : creds.accessTokenFieldName;
refreshTokenFieldName = creds === null || creds === void 0 ? void 0 : creds.refreshTokenFieldName;
}
catch {
}
const extraSensitiveKeys = [];
if (accessTokenFieldName)
extraSensitiveKeys.push(accessTokenFieldName);
if (refreshTokenFieldName)
extraSensitiveKeys.push(refreshTokenFieldName);
for (let i = 0; i < items.length; i++) {
const url = this.getNodeParameter('url', i);
const method = this.getNodeParameter('method', i);
const qs = this.getNodeParameter('qsJson', i, {});
const headersExtra = this.getNodeParameter('headersJson', i, {});
const sendPreauth = this.getNodeParameter('sendPreauth', i, true);
const sendMain = this.getNodeParameter('sendMain', i, true);
const redact = this.getNodeParameter('redact', i, false);
const truncLen = this.getNodeParameter('truncate', i, 12000);
const body = method === 'GET' ? undefined : this.getNodeParameter('bodyJson', i, {});
const events = [];
const originalHttpRequest = this.helpers.httpRequest.bind(this.helpers);
const wrappedHttpRequest = async (opts) => {
var _a, _b, _c, _d, _e, _f;
const reqUrl = String(opts.url || opts.baseURL || '');
const reqBody = (_b = (_a = opts.form) !== null && _a !== void 0 ? _a : opts.body) !== null && _b !== void 0 ? _b : opts.json;
const isPre = looksLikePreAuth(reqUrl, reqBody, tokenUrlFromCreds);
const shouldSend = isPre ? sendPreauth : sendMain;
events.push({
stage: 'request',
ts: new Date().toISOString(),
source: 'helper',
url: reqUrl,
method: String(opts.method || 'GET').toUpperCase(),
headers: { ...(opts.headers || {}) },
body: safeParseBody(reqBody),
isPreAuth: isPre,
});
if (!shouldSend) {
const mock = {
body: { mocked: true, sent: false, reason: 'SKIPPED_BY_NODE' },
headers: {},
statusCode: 0,
statusMessage: 'MOCKED-NOT-SENT',
};
events.push({
stage: 'response',
ts: new Date().toISOString(),
source: 'helper',
url: reqUrl,
statusCode: 0,
statusMessage: 'MOCKED-NOT-SENT',
headers: {},
body: mock.body,
isPreAuth: isPre,
});
return opts.returnFullResponse ? mock : mock.body;
}
const result = await originalHttpRequest(opts);
const respBody = (_c = result.body) !== null && _c !== void 0 ? _c : result;
events.push({
stage: 'response',
ts: new Date().toISOString(),
source: 'helper',
url: reqUrl,
statusCode: (_d = result.statusCode) !== null && _d !== void 0 ? _d : 200,
statusMessage: (_e = result.statusMessage) !== null && _e !== void 0 ? _e : 'OK',
headers: (_f = result.headers) !== null && _f !== void 0 ? _f : {},
body: safeParseBody(respBody),
isPreAuth: isPre,
});
return result;
};
this.helpers.httpRequest = wrappedHttpRequest;
const reqOpts = {
method: method,
url,
qs,
headers: { ...headersExtra },
json: true,
body,
returnFullResponse: true,
};
try {
await this.helpers.httpRequestWithAuthentication.call(this, CREDENTIALS_NAME, reqOpts);
}
catch (err) {
out.push({
json: {
error: String((err === null || err === void 0 ? void 0 : err.message) || err),
events: events.map((e) => formatEvent(e, redact, truncLen, extraSensitiveKeys)),
note: 'Main request errored (often expected if sending disabled).',
},
});
continue;
}
finally {
this.helpers.httpRequest = originalHttpRequest;
}
out.push({
json: { events: events.map((e) => formatEvent(e, redact, truncLen, extraSensitiveKeys)) },
});
}
return [out];
}
}
exports.DebugRefreshToken = DebugRefreshToken;
//# sourceMappingURL=DebugRefreshToken.node.js.map