UNPKG

n8n-nodes-refresh-token-auth

Version:
585 lines 25.7 kB
"use strict"; 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