n8n-nodes-refresh-token-auth
Version:
n8n community node with automatic refresh token support
585 lines • 25.7 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RefreshTokenAuth = void 0;
const n8n_workflow_1 = require("n8n-workflow");
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
function getNestedValue(obj, path) {
if (!path || !obj)
return undefined;
const traverse = (target, keys) => keys.reduce((curr, key) => (curr && typeof curr === 'object' && key in curr ? curr[key] : undefined), target);
const keys = path.split('.');
const directValue = traverse(obj, keys);
if (directValue !== undefined)
return directValue;
const commonPrefixes = ['body', 'data', 'response'];
for (const prefix of commonPrefixes) {
if (obj[prefix] && typeof obj[prefix] === 'object') {
const prefixedValue = traverse(obj[prefix], keys);
if (prefixedValue !== undefined)
return prefixedValue;
}
}
return undefined;
}
function replacePlaceholders(value, credentials) {
if (typeof value !== 'string')
return value;
return value
.replace(/\{\{\$credentials\.accessToken\}\}/g, credentials.accessToken || '')
.replace(/\{\{\$credentials\.refreshToken\}\}/g, credentials.refreshToken || '');
}
function replacePlaceholdersInObject(obj, credentials) {
if (typeof obj !== 'object' || obj === null)
return replacePlaceholders(obj, credentials);
if (Array.isArray(obj))
return obj.map((item) => replacePlaceholdersInObject(item, credentials));
return Object.fromEntries(Object.entries(obj).map(([key, val]) => [key, replacePlaceholdersInObject(val, credentials)]));
}
function mergeObjectFields(target, source, fields, sourceOverrides = true) {
for (const field of fields) {
if (source[field]) {
const targetObj = target;
const [first, second] = sourceOverrides
? [targetObj[field], source[field]]
: [source[field], targetObj[field]];
targetObj[field] = { ...first, ...second };
}
}
}
function applyJsonTemplate(requestOptions, jsonString, errorMessage, fields, sourceOverrides = true) {
if (!jsonString)
return;
try {
const template = (0, n8n_workflow_1.jsonParse)(jsonString, { errorMessage });
mergeObjectFields(requestOptions, template, fields, sourceOverrides);
}
catch {
}
}
function tryParseJwtPayload(token) {
if (typeof token !== 'string' || token.trim() === '') {
return null;
}
try {
const decoded = jsonwebtoken_1.default.decode(token, { complete: true });
if (!decoded || typeof decoded === 'string' || !decoded.payload) {
return null;
}
return decoded.payload;
}
catch {
return null;
}
}
function extractExpiresIn(response, fieldName) {
if (!fieldName)
return undefined;
const value = getNestedValue(response, fieldName);
if (value === undefined || value === null)
return undefined;
const numValue = typeof value === 'string' ? parseFloat(value) : Number(value);
return isNaN(numValue) ? undefined : numValue;
}
function convertExpiresInToUnixTimestamp(expiresIn, format) {
if (expiresIn === undefined || expiresIn === null)
return undefined;
if (isNaN(expiresIn))
return undefined;
const now = Date.now();
const nowSeconds = Math.floor(now / 1000);
switch (format) {
case 'seconds':
return nowSeconds + Math.floor(expiresIn);
case 'milliseconds':
return nowSeconds + Math.floor(expiresIn / 1000);
case 'microseconds':
return nowSeconds + Math.floor(expiresIn / 1000000);
case 'unix-seconds':
return Math.floor(expiresIn);
case 'unix-milliseconds':
return Math.floor(expiresIn / 1000);
default:
return nowSeconds + Math.floor(expiresIn);
}
}
class RefreshTokenAuth {
constructor() {
this.name = 'refreshTokenAuth';
this.displayName = 'Refresh Token Auth';
this.icon = 'node:n8n-nodes-base.httpRequest';
this.documentationUrl = 'https://github.com/rctphone/n8n-nodes-refresh-token-auth';
this.properties = [
{
displayName: 'Access Token',
name: 'accessToken',
type: 'string',
typeOptions: { expirable: true, password: true },
default: '',
placeholder: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
description: 'Current access token (Bearer token) used for API authentication',
},
{
displayName: 'Expires In Timestamp',
name: 'expiresInUnixTimestamp',
type: 'hidden',
displayOptions: {
show: {
refreshTokenMode: ['onJwtExpiry'],
},
},
disabledOptions: {
hide: {
refreshTokenMode: ['onJwtExpiry'],
},
},
default: '',
description: 'Expiration timestamp (Unix seconds) from refresh response, used when Expiration Source is "From Refresh Response". This field is automatically updated and should not be edited manually.',
},
{
displayName: 'Refresh Token Mode',
name: 'refreshTokenMode',
type: 'options',
default: 'onJwtExpiry',
description: 'When to trigger token refresh',
options: [
{
name: 'Never (Manual Only)',
value: 'never',
description: 'Do not refresh token automatically',
},
{
name: 'Always',
value: 'always',
description: 'Always refresh token before each request',
},
{
name: 'On JWT Expiry',
value: 'onJwtExpiry',
description: 'Refresh token when JWT exp claim indicates expiration',
},
{
name: 'On 401 Error',
value: 'onTestEndpoint401',
description: 'Refresh token when API returns 401 Unauthorized',
},
],
},
{
displayName: 'Expiration Source',
name: 'expiresInSource',
type: 'options',
default: 'jwt',
description: 'Source for token expiration time (only used with "On JWT Expiry" mode)',
displayOptions: {
show: {
refreshTokenMode: ['onJwtExpiry'],
},
},
options: [
{
name: 'From JWT Token',
value: 'jwt',
description: 'Extract expiration from JWT token exp claim',
},
{
name: 'From Refresh Response',
value: 'refreshResponse',
description: 'Extract expiration from refresh response field',
},
],
},
{
displayName: 'Expires In Field Name',
name: 'expiresInFieldName',
type: 'string',
default: 'expires_in',
description: 'Field name in refresh response containing expiration time (supports dot notation, e.g., "data.expires_in")',
displayOptions: {
show: {
refreshTokenMode: ['onJwtExpiry'],
expiresInSource: ['refreshResponse'],
},
},
},
{
displayName: 'Expires In Format',
name: 'expiresInFormat',
type: 'options',
default: 'seconds',
description: 'Format of expiration time value in refresh response (only used when Expiration Source is "From Refresh Response")',
displayOptions: {
show: {
refreshTokenMode: ['onJwtExpiry'],
expiresInSource: ['refreshResponse'],
},
},
options: [
{
name: 'Seconds (relative)',
value: 'seconds',
description: 'Relative time in seconds from now (e.g., 3600 = 1 hour)',
},
{
name: 'Milliseconds (relative)',
value: 'milliseconds',
description: 'Relative time in milliseconds from now',
},
{
name: 'Microseconds (relative)',
value: 'microseconds',
description: 'Relative time in microseconds from now',
},
{
name: 'Unix Timestamp (seconds)',
value: 'unix-seconds',
description: 'Absolute Unix timestamp in seconds',
},
{
name: 'Unix Timestamp (milliseconds)',
value: 'unix-milliseconds',
description: 'Absolute Unix timestamp in milliseconds',
},
],
},
{
displayName: 'Refresh Token',
name: 'refreshToken',
type: 'string',
required: true,
typeOptions: { password: true },
default: '',
placeholder: 'Enter your refresh token',
description: 'Token used to obtain a new access token when it expires',
},
{
displayName: 'Refresh Token URL',
name: 'refreshUrl',
type: 'string',
required: true,
default: '',
placeholder: 'https://api.example.com/auth/refresh',
description: 'API endpoint URL to refresh the access token',
},
{
displayName: 'Test URL',
name: 'testUrl',
type: 'string',
required: true,
default: '',
placeholder: 'https://api.example.com/user/profile',
description: 'API endpoint URL to test the token validity (should return HTTP 200)',
},
{
displayName: 'Access Token Field Name',
name: 'accessTokenFieldName',
type: 'string',
typeOptions: { password: false },
default: 'access_token',
description: 'Field name in the refresh response that contains the new access token (supports dot notation, e.g., "data.token")',
},
{
displayName: 'Refresh Token Field Name',
name: 'refreshTokenFieldName',
type: 'string',
typeOptions: { password: false },
default: 'refresh_token',
description: 'Field name for refresh token (used in both API request and response, supports dot notation such as "data.refreshToken")',
},
{
displayName: 'Authorization Header Prefix',
name: 'authHeaderPrefix',
type: 'string',
default: 'Bearer',
description: 'Prefix for the Authorization header (e.g., "Bearer", "Token")',
},
{
displayName: 'Refresh Request Configuration',
name: 'refreshRequestJson',
type: 'json',
required: false,
description: 'JSON configuration for refresh token request',
placeholder: `{"headers": {...}, "body": {...}, "qs": {...}}`,
default: '',
},
{
displayName: `JSON configuration for refresh token request<br />
<br />
Supported placeholders:<br />
• {{$credentials.accessToken}}<br />
• {{$credentials.refreshToken}}<br />
<br />
Example:<br />
<pre>{
"headers": { "User-Agent": "MyApp/1.0" },
"body": {
"grant_type": "refresh_token",
"refresh_token": "{{$credentials.refreshToken}}",
"client_id": "your_id",
"client_secret": "your_secret"
},
"qs": {}
}</pre>`,
name: 'refreshRequestJsonNotice',
type: 'notice',
default: '',
},
{
displayName: 'Common Request Template',
name: 'commonRequestTemplate',
type: 'json',
required: false,
description: 'JSON template for headers and query parameters applied to ALL requests (refresh, test, main)',
placeholder: `{"headers": {...}, "qs": {...}}`,
default: '',
},
{
displayName: `JSON template for headers and query parameters applied to ALL requests (refresh, test, main)<br />
<br />Supported placeholders:<br />
• {{$credentials.accessToken}}<br />
• {{$credentials.refreshToken}}<br />
<br />
Example:<br />
<pre>{
"headers": {
"User-Agent": "MyApp/1.0",
"X-Device-Id": "device123"
},
"qs": { "version": "v1" }
}</pre>`,
name: 'commonRequestTemplateNotice',
type: 'notice',
default: '',
},
{
displayName: 'JWT Expiry Leeway (seconds)',
name: 'jwtExpiryLeewaySeconds',
type: 'number',
default: 60,
description: 'Number of seconds before JWT expiration to trigger refresh (only used with "On JWT Expiry" mode)',
displayOptions: {
show: {
refreshTokenMode: ['onJwtExpiry'],
},
},
},
{
displayName: 'Ignore SSL Issues (Insecure)',
name: 'allowUnauthorizedCerts',
type: 'boolean',
default: false,
description: 'Whether to skip SSL certificate validation for refresh and test requests (use with caution)',
},
{
displayName: 'Extract Cookies from Refresh Response',
name: 'extractCookies',
type: 'boolean',
default: false,
description: 'Whether to extract Set-Cookie headers from refresh response and include them in subsequent requests',
},
{
displayName: 'Hidden Field for Refreshing Logics',
name: 'hidden',
type: 'hidden',
typeOptions: { expirable: true },
default: '',
placeholder: '',
description: 'Hidden field needed for refreshing logics, preAuth should return empty string to run again, do not remove!!!',
},
{
displayName: 'Hidden Field for Stored Cookies',
name: 'storedCookies',
type: 'hidden',
default: '',
placeholder: '',
description: 'Hidden field to store cookies from Set-Cookie header received during refresh token request',
},
];
this.authenticate = {
type: 'generic',
properties: {
headers: {
Authorization: '={{$credentials.authHeaderPrefix}} {{$credentials.accessToken}}',
},
},
};
this.test = {
request: {
url: '={{$credentials.testUrl}}',
method: 'GET',
skipSslCertificateValidation: '={{$credentials.allowUnauthorizedCerts}}',
},
};
RefreshTokenAuth.instance = this;
}
static enableAuthenticateFunc() {
const credentialType = RefreshTokenAuth.instance;
credentialType.authenticate = credentialType.authenticateFunc;
}
async authenticateFunc(credentials, requestOptions) {
var _a;
applyJsonTemplate(requestOptions, credentials.commonRequestTemplate, 'Invalid Common Request Template JSON', ['headers', 'qs'], false);
const prefix = credentials.authHeaderPrefix || 'Bearer';
const separator = prefix === 'Bearer' ? ' ' : '';
requestOptions.headers = {
...requestOptions.headers,
Authorization: `${prefix}${separator}${credentials.accessToken}`,
};
const extractCookies = credentials.extractCookies === true;
const storedCookies = credentials.storedCookies;
if (extractCookies && storedCookies) {
const existingCookie = (_a = requestOptions.headers) === null || _a === void 0 ? void 0 : _a.Cookie;
const mergedCookies = existingCookie ? `${existingCookie}; ${storedCookies}` : storedCookies;
requestOptions.headers = {
...requestOptions.headers,
Cookie: mergedCookies,
};
}
const allowUnauthorizedCerts = credentials.allowUnauthorizedCerts === true || credentials.allowUnauthorizedCerts === 'true';
if (allowUnauthorizedCerts) {
requestOptions.skipSslCertificateValidation = true;
}
return requestOptions;
}
async preAuthentication(credentials) {
var _a, _b, _c;
RefreshTokenAuth.enableAuthenticateFunc();
const accessToken = credentials.accessToken;
const refreshTokenMode = credentials.refreshTokenMode || 'onJwtExpiry';
const jwtExpiryLeewaySeconds = credentials.jwtExpiryLeewaySeconds || 60;
const expiresInSource = credentials.expiresInSource || 'jwt';
const shouldRefresh = (() => {
switch (refreshTokenMode) {
case 'never':
case 'onTestEndpoint401':
return false;
case 'always':
return true;
case 'onJwtExpiry': {
if (expiresInSource === 'refreshResponse') {
const storedExpiresIn = credentials.expiresInUnixTimestamp;
if (!storedExpiresIn || storedExpiresIn === '') {
return true;
}
const expiresInTimestamp = parseInt(storedExpiresIn, 10);
if (isNaN(expiresInTimestamp)) {
return true;
}
const nowSeconds = Math.floor(Date.now() / 1000);
return expiresInTimestamp - nowSeconds <= jwtExpiryLeewaySeconds;
}
else {
const payload = tryParseJwtPayload(accessToken);
if (!payload)
return true;
const exp = payload.exp;
if (!exp)
return true;
return exp - Math.floor(Date.now() / 1000) <= jwtExpiryLeewaySeconds;
}
}
default:
return false;
}
})();
if (!shouldRefresh)
return {};
const allowUnauthorizedCerts = credentials.allowUnauthorizedCerts === true || credentials.allowUnauthorizedCerts === 'true';
const requestOptions = {
url: credentials.refreshUrl,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
skipSslCertificateValidation: allowUnauthorizedCerts === true ? true : undefined,
};
applyJsonTemplate(requestOptions, credentials.commonRequestTemplate, 'Invalid Common Request Template JSON', ['headers', 'qs']);
const auth = replacePlaceholdersInObject((0, n8n_workflow_1.jsonParse)(credentials.refreshRequestJson || '{}', {
errorMessage: 'Invalid Refresh Request Configuration JSON',
}), credentials);
const objectFieldsToMerge = ['headers', 'body', 'qs'];
for (const key of Object.keys(auth)) {
if (!objectFieldsToMerge.includes(key)) {
requestOptions[key] = auth[key];
}
}
mergeObjectFields(requestOptions, auth, objectFieldsToMerge);
requestOptions.returnFullResponse = true;
try {
const fullResponse = await this.helpers.httpRequest(requestOptions);
const responseHeaders = fullResponse.headers || {};
const accessTokenField = credentials.accessTokenFieldName || 'access_token';
const refreshTokenField = credentials.refreshTokenFieldName || 'refresh_token';
const newAccessToken = getNestedValue(fullResponse, accessTokenField);
if (!newAccessToken)
throw new Error('Access token not found in response');
const expiresInSource = credentials.expiresInSource || 'jwt';
let expiresInUnixTimestamp;
if (refreshTokenMode === 'onJwtExpiry' && expiresInSource === 'refreshResponse') {
const expiresInFieldName = credentials.expiresInFieldName || 'expires_in';
const expiresInFormat = credentials.expiresInFormat || 'seconds';
const expiresInValue = extractExpiresIn(fullResponse, expiresInFieldName);
const expiresInTimestamp = convertExpiresInToUnixTimestamp(expiresInValue, expiresInFormat);
if (expiresInTimestamp !== undefined) {
expiresInUnixTimestamp = expiresInTimestamp.toString();
}
}
const extractCookies = credentials.extractCookies === true;
let storedCookies;
if (extractCookies) {
const setCookieHeader = responseHeaders['set-cookie'] || responseHeaders['Set-Cookie'];
if (setCookieHeader) {
const cookieArray = Array.isArray(setCookieHeader) ? setCookieHeader : [setCookieHeader];
storedCookies = cookieArray
.map((cookie) => cookie.split(';')[0].trim())
.join('; ');
}
}
const result = {
accessToken: newAccessToken,
refreshToken: getNestedValue(fullResponse, refreshTokenField) || credentials.refreshToken,
hidden: '',
};
if (expiresInUnixTimestamp !== undefined) {
result.expiresInUnixTimestamp = expiresInUnixTimestamp;
}
if (storedCookies) {
result.storedCookies = storedCookies;
}
return result;
}
catch (error) {
const errorLines = [`Token refresh failed: ${error.message}`];
const axiosConfig = error.config;
const method = ((_a = axiosConfig === null || axiosConfig === void 0 ? void 0 : axiosConfig.method) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || requestOptions.method;
const url = ((_c = (_b = error.request) === null || _b === void 0 ? void 0 : _b._redirectable) === null || _c === void 0 ? void 0 : _c._currentUrl) || (axiosConfig === null || axiosConfig === void 0 ? void 0 : axiosConfig.url);
errorLines.push(`Request: ${method} ${url}`);
const configHeaders = axiosConfig === null || axiosConfig === void 0 ? void 0 : axiosConfig.headers;
if (configHeaders) {
const headers = { ...configHeaders };
if (headers.Authorization)
headers.Authorization = '[MASKED]';
if (headers.authorization)
headers.authorization = '[MASKED]';
errorLines.push(`Request headers: ${JSON.stringify(headers)}`);
}
if (error.response) {
const status = error.response.status || error.response.statusCode;
const statusText = error.response.statusText || error.response.statusMessage || '';
errorLines.push(`Response: ${status} ${statusText}`);
const responseBody = error.response.body || error.response.data;
if (responseBody) {
const bodyStr = typeof responseBody === 'string' ? responseBody : JSON.stringify(responseBody);
const truncatedBody = bodyStr.length > 500 ? bodyStr.substring(0, 500) + '...' : bodyStr;
errorLines.push(`Response body: ${truncatedBody}`);
}
}
if (error.code) {
errorLines.push(`Error code: ${error.code}`);
}
throw new Error(errorLines.join(' | '));
}
}
}
exports.RefreshTokenAuth = RefreshTokenAuth;
//# sourceMappingURL=RefreshTokenAuth.credentials.js.map