Dynamic Credentials in n8n
Dynamic credentials allow your nodes to adapt authentication methods at runtime, support multiple auth providers, and integrate with enterprise systems without hardcoding credentials.
- Multi-tenant authentication systems
- Runtime OAuth provider selection
- Credential delegation and impersonation
- Vault integration for secret management
- Just-in-time credential provisioning
Building Dynamic Credential Systems
Basic Dynamic Credential Node
Copy
Ask AI
import {
ICredentialType,
INodeProperties,
INodeExecutionData,
IExecuteFunctions,
ICredentialDataDecryptedObject,
} from 'n8n-workflow';
export class DynamicAuthNode {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
// Get auth method selected by user
const authMethod = this.getNodeParameter('authMethod', 0) as string;
// Dynamically load credentials based on selection
const credentials = await this.loadDynamicCredentials(authMethod);
// Use credentials to make API call
const result = await this.makeAuthenticatedRequest(credentials);
return [[{ json: result }]];
}
private async loadDynamicCredentials(
authMethod: string
): Promise<ICredentialDataDecryptedObject> {
switch (authMethod) {
case 'oauth2':
return await this.getCredentials('oAuth2Api');
case 'apiKey':
return await this.getCredentials('apiKeyAuth');
case 'vault':
return await this.loadFromVault();
case 'delegated':
return await this.getDelegatedCredentials();
default:
throw new Error(`Unknown auth method: ${authMethod}`);
}
}
private async loadFromVault(): Promise<ICredentialDataDecryptedObject> {
// Connect to HashiCorp Vault or AWS Secrets Manager
const vaultEndpoint = this.getNodeParameter('vaultEndpoint', 0) as string;
const vaultToken = process.env.VAULT_TOKEN;
const vault = new Vault({
endpoint: vaultEndpoint,
token: vaultToken
});
const secret = await vault.read('secret/data/api-credentials');
return {
apiKey: secret.data.api_key,
apiSecret: secret.data.api_secret,
environment: secret.data.environment
};
}
}
Multi-Provider OAuth Implementation
When implementing OAuth, always validate redirect URIs and use state parameters to prevent CSRF attacks. Store refresh tokens securely and implement token rotation.
Copy
Ask AI
export class MultiProviderOAuth implements ICredentialType {
name = 'multiProviderOAuth';
displayName = 'Multi-Provider OAuth';
documentationUrl = 'oauth';
properties: INodeProperties[] = [
{
displayName: 'OAuth Provider',
name: 'provider',
type: 'options',
options: [
{ name: 'Google', value: 'google' },
{ name: 'Microsoft', value: 'microsoft' },
{ name: 'Custom', value: 'custom' }
],
default: 'google',
},
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'string',
displayOptions: {
show: {
provider: ['custom'],
},
},
default: '',
},
];
async authenticate(
credentials: ICredentialDataDecryptedObject,
requestOptions: any
): Promise<any> {
const provider = credentials.provider as string;
// Load provider-specific configuration
const config = await this.getProviderConfig(provider, credentials);
// Build OAuth flow dynamically
const oauth = new OAuth2(
config.clientId,
config.clientSecret,
config.authUrl,
config.tokenUrl,
config.scopes
);
// Handle token refresh automatically
if (this.isTokenExpired(credentials.expiresAt)) {
const newTokens = await oauth.refreshAccessToken(credentials.refreshToken);
await this.updateStoredCredentials(newTokens);
requestOptions.headers.Authorization = `Bearer ${newTokens.accessToken}`;
} else {
requestOptions.headers.Authorization = `Bearer ${credentials.accessToken}`;
}
return requestOptions;
}
private async getProviderConfig(
provider: string,
credentials: any
): Promise<OAuthConfig> {
const configs: { [key: string]: OAuthConfig } = {
google: {
authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
scopes: ['https://www.googleapis.com/auth/drive'],
clientId: credentials.clientId,
clientSecret: credentials.clientSecret,
},
microsoft: {
authUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
scopes: ['https://graph.microsoft.com/.default'],
clientId: credentials.clientId,
clientSecret: credentials.clientSecret,
},
custom: {
authUrl: credentials.authUrl,
tokenUrl: credentials.tokenUrl,
scopes: credentials.scopes?.split(',') || [],
clientId: credentials.clientId,
clientSecret: credentials.clientSecret,
},
};
return configs[provider] || configs.custom;
}
}
Dynamic NPM Module Loading
Runtime Module Installation
Sometimes you need to load NPM modules that arenβt bundled with n8n. Dynamic loading allows users to specify packages that your node will install and use at runtime.
Copy
Ask AI
import { execSync } from 'child_process';
import * as path from 'path';
import * as fs from 'fs';
export class DynamicModuleLoader {
private moduleCache = new Map<string, any>();
private modulePath = path.join(process.cwd(), 'dynamic_modules');
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const moduleName = this.getNodeParameter('npmModule', 0) as string;
const moduleVersion = this.getNodeParameter('moduleVersion', 0, 'latest') as string;
const functionName = this.getNodeParameter('functionName', 0) as string;
// Load module dynamically
const module = await this.loadModule(moduleName, moduleVersion);
// Execute specified function
const result = await this.executeModuleFunction(module, functionName);
return [[{ json: { result } }]];
}
private async loadModule(name: string, version: string): Promise<any> {
const cacheKey = `${name}@${version}`;
// Check cache first
if (this.moduleCache.has(cacheKey)) {
return this.moduleCache.get(cacheKey);
}
// Check if module is already installed
const modulePath = path.join(this.modulePath, 'node_modules', name);
if (!fs.existsSync(modulePath)) {
await this.installModule(name, version);
}
// Load the module
const module = await this.requireModule(modulePath);
// Cache for future use
this.moduleCache.set(cacheKey, module);
return module;
}
private async installModule(name: string, version: string): Promise<void> {
const packageSpec = `${name}@${version}`;
// Validate module name to prevent injection
if (!this.isValidModuleName(name)) {
throw new Error(`Invalid module name: ${name}`);
}
// Check against allowlist
if (!this.isAllowedModule(name)) {
throw new Error(`Module not in allowlist: ${name}`);
}
// Install in isolated directory
try {
execSync(`npm install ${packageSpec} --prefix ${this.modulePath}`, {
timeout: 30000, // 30 second timeout
stdio: 'pipe'
});
} catch (error) {
throw new Error(`Failed to install module ${packageSpec}: ${error.message}`);
}
}
private async requireModule(modulePath: string): Promise<any> {
try {
// Clear require cache for hot-reloading
delete require.cache[require.resolve(modulePath)];
// Load module with sandboxing
const module = require(modulePath);
// Wrap in proxy for security
return this.createSecureProxy(module);
} catch (error) {
throw new Error(`Failed to load module: ${error.message}`);
}
}
private createSecureProxy(module: any): any {
return new Proxy(module, {
get: (target, prop) => {
// Block dangerous properties
const blockedProps = ['exec', 'execSync', 'spawn', 'spawnSync'];
if (blockedProps.includes(String(prop))) {
throw new Error(`Access to ${String(prop)} is blocked`);
}
return target[prop];
}
});
}
private isValidModuleName(name: string): boolean {
// Prevent path traversal and command injection
const validPattern = /^[@a-z0-9][\w.-]*$/i;
return validPattern.test(name) && !name.includes('/') && !name.includes('\\');
}
private isAllowedModule(name: string): boolean {
const allowlist = [
'lodash',
'moment',
'axios',
'cheerio',
'csv-parser',
'xml2js',
// Add more safe modules
];
return allowlist.includes(name);
}
}
Secure Module Sandboxing
Copy
Ask AI
import * as vm from 'vm';
export class SandboxedModuleExecutor {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const code = this.getNodeParameter('code', 0) as string;
const modules = this.getNodeParameter('modules', 0) as string[];
// Create sandboxed environment
const result = await this.runInSandbox(code, modules);
return [[{ json: result }]];
}
private async runInSandbox(
code: string,
allowedModules: string[]
): Promise<any> {
// Create context with limited globals
const sandbox = {
console: {
log: (...args: any[]) => this.logger.info(...args),
error: (...args: any[]) => this.logger.error(...args),
},
setTimeout: undefined, // Block timers
setInterval: undefined,
setImmediate: undefined,
process: {
env: {}, // Empty environment
version: process.version,
},
require: this.createSecureRequire(allowedModules),
result: null,
};
// Create VM context
const context = vm.createContext(sandbox);
// Wrap code in async function
const wrappedCode = `
(async () => {
${code}
})().then(r => result = r).catch(e => result = { error: e.message });
`;
try {
// Run with timeout
const script = new vm.Script(wrappedCode);
await script.runInContext(context, {
timeout: 5000, // 5 second timeout
breakOnSigint: true,
});
// Wait for async completion
await new Promise(resolve => setTimeout(resolve, 100));
return sandbox.result;
} catch (error) {
throw new Error(`Sandbox execution failed: ${error.message}`);
}
}
private createSecureRequire(allowedModules: string[]) {
return (moduleName: string) => {
if (!allowedModules.includes(moduleName)) {
throw new Error(`Module ${moduleName} is not allowed`);
}
try {
return require(moduleName);
} catch (error) {
throw new Error(`Failed to load module ${moduleName}`);
}
};
}
}
Enterprise Credential Management
Credential Rotation System
Copy
Ask AI
export class CredentialRotationNode {
private rotationSchedule = new Map<string, Date>();
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const credentialId = this.getNodeParameter('credentialId', 0) as string;
const rotationPolicy = this.getNodeParameter('rotationPolicy', 0) as any;
// Check if rotation is needed
if (await this.needsRotation(credentialId, rotationPolicy)) {
await this.rotateCredentials(credentialId);
}
// Use current credentials
const credentials = await this.getCurrentCredentials(credentialId);
return await this.executeWithCredentials(credentials);
}
private async needsRotation(
credentialId: string,
policy: any
): Promise<boolean> {
const lastRotation = this.rotationSchedule.get(credentialId);
if (!lastRotation) {
return true; // Never rotated
}
const daysSinceRotation =
(Date.now() - lastRotation.getTime()) / (1000 * 60 * 60 * 24);
return daysSinceRotation >= policy.rotationDays;
}
private async rotateCredentials(credentialId: string): Promise<void> {
try {
// Generate new credentials
const newCredentials = await this.generateNewCredentials();
// Update in parallel: new active, old as backup
await Promise.all([
this.updateActiveCredentials(credentialId, newCredentials),
this.archiveOldCredentials(credentialId),
]);
// Update rotation schedule
this.rotationSchedule.set(credentialId, new Date());
// Notify administrators
await this.notifyRotation(credentialId);
} catch (error) {
this.logger.error('Credential rotation failed:', error);
throw error;
}
}
private async generateNewCredentials(): Promise<any> {
// Generate cryptographically secure credentials
const crypto = require('crypto');
return {
apiKey: crypto.randomBytes(32).toString('hex'),
apiSecret: crypto.randomBytes(64).toString('hex'),
generatedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
};
}
}
Delegation and Impersonation
Copy
Ask AI
export class DelegatedAccessNode {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const delegationType = this.getNodeParameter('delegationType', 0) as string;
const targetUser = this.getNodeParameter('targetUser', 0) as string;
// Get delegation token
const delegationToken = await this.getDelegationToken(
delegationType,
targetUser
);
// Execute with delegated permissions
return await this.executeAsDelegatedUser(delegationToken);
}
private async getDelegationToken(
type: string,
targetUser: string
): Promise<string> {
switch (type) {
case 'serviceAccount':
return await this.getServiceAccountToken(targetUser);
case 'impersonation':
return await this.getImpersonationToken(targetUser);
case 'oauth2Delegation':
return await this.getOAuth2DelegationToken(targetUser);
default:
throw new Error(`Unknown delegation type: ${type}`);
}
}
private async getServiceAccountToken(user: string): Promise<string> {
// Google Service Account delegation example
const { JWT } = require('google-auth-library');
const serviceAccount = await this.getCredentials('googleServiceAccount');
const client = new JWT({
email: serviceAccount.email,
key: serviceAccount.privateKey,
scopes: ['https://www.googleapis.com/auth/drive'],
subject: user, // Impersonate this user
});
const { access_token } = await client.getAccessToken();
return access_token;
}
private async getOAuth2DelegationToken(user: string): Promise<string> {
// Microsoft Graph delegation example
const credentials = await this.getCredentials('microsoftOAuth2');
const tokenEndpoint = 'https://login.microsoftonline.com/common/oauth2/v2.0/token';
const response = await this.helpers.httpRequest({
method: 'POST',
url: tokenEndpoint,
body: {
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
client_id: credentials.clientId,
client_secret: credentials.clientSecret,
assertion: await this.createDelegationAssertion(user),
requested_token_use: 'on_behalf_of',
scope: 'https://graph.microsoft.com/.default',
},
});
return response.access_token;
}
}
Advanced Module Patterns
Plugin Architecture
Copy
Ask AI
export class PluginNode {
private plugins = new Map<string, Plugin>();
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const pluginPath = this.getNodeParameter('pluginPath', 0) as string;
const pluginConfig = this.getNodeParameter('pluginConfig', 0) as any;
// Load and initialize plugin
const plugin = await this.loadPlugin(pluginPath, pluginConfig);
// Execute plugin
return await plugin.execute(this);
}
private async loadPlugin(
path: string,
config: any
): Promise<Plugin> {
// Check cache
if (this.plugins.has(path)) {
return this.plugins.get(path)!;
}
// Validate plugin
await this.validatePlugin(path);
// Load plugin module
const PluginClass = await import(path);
// Initialize with config
const plugin = new PluginClass(config);
// Verify plugin interface
if (!this.implementsPluginInterface(plugin)) {
throw new Error('Invalid plugin interface');
}
// Cache plugin
this.plugins.set(path, plugin);
return plugin;
}
private implementsPluginInterface(plugin: any): boolean {
return (
typeof plugin.execute === 'function' &&
typeof plugin.validate === 'function' &&
typeof plugin.getMetadata === 'function'
);
}
private async validatePlugin(path: string): Promise<void> {
// Security validation
const fs = require('fs').promises;
const crypto = require('crypto');
// Check plugin signature
const pluginContent = await fs.readFile(path, 'utf8');
const signature = await fs.readFile(`${path}.sig`, 'utf8');
const isValid = this.verifySignature(pluginContent, signature);
if (!isValid) {
throw new Error('Plugin signature verification failed');
}
}
}
interface Plugin {
execute(context: IExecuteFunctions): Promise<INodeExecutionData[][]>;
validate(input: any): boolean;
getMetadata(): PluginMetadata;
}
Lazy Loading Strategy
Copy
Ask AI
export class LazyLoadingNode {
private moduleLoaders = new Map<string, () => Promise<any>>();
constructor() {
// Register module loaders
this.registerModuleLoaders();
}
private registerModuleLoaders(): void {
// Define lazy loaders for heavy modules
this.moduleLoaders.set('tensorflow', async () => {
const tf = await import('@tensorflow/tfjs-node');
await tf.ready();
return tf;
});
this.moduleLoaders.set('sharp', async () => {
return await import('sharp');
});
this.moduleLoaders.set('puppeteer', async () => {
const puppeteer = await import('puppeteer');
return puppeteer.default;
});
}
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const operation = this.getNodeParameter('operation', 0) as string;
// Load only required modules
const requiredModules = this.getRequiredModules(operation);
const modules = await this.loadModules(requiredModules);
// Execute with loaded modules
return await this.executeOperation(operation, modules);
}
private getRequiredModules(operation: string): string[] {
const moduleMap: { [key: string]: string[] } = {
'imageProcessing': ['sharp'],
'machineLearning': ['tensorflow'],
'webScraping': ['puppeteer'],
};
return moduleMap[operation] || [];
}
private async loadModules(
moduleNames: string[]
): Promise<Map<string, any>> {
const loaded = new Map<string, any>();
for (const name of moduleNames) {
const loader = this.moduleLoaders.get(name);
if (loader) {
this.logger.info(`Loading module: ${name}`);
loaded.set(name, await loader());
}
}
return loaded;
}
}
Security Best Practices
Module Security Checklist
Security Implementation Guide
Security Implementation Guide
-
Input Validation
CopyAsk AI
// Always validate module names const validModulePattern = /^[@a-z0-9][\w.-]*$/i; if (!validModulePattern.test(moduleName)) { throw new Error('Invalid module name'); }
-
Use Allowlists
CopyAsk AI
const allowedModules = new Set(['lodash', 'moment', 'axios']); if (!allowedModules.has(moduleName)) { throw new Error('Module not allowed'); }
-
Sandbox Execution
CopyAsk AI
const vm = require('vm'); const sandbox = { /* limited context */ }; vm.runInContext(code, sandbox, { timeout: 5000 });
-
Version Pinning
CopyAsk AI
// Always use exact versions in production const moduleSpec = `${name}@${exactVersion}`;
-
Signature Verification
CopyAsk AI
const crypto = require('crypto'); const verify = crypto.createVerify('RSA-SHA256'); verify.update(moduleContent); const isValid = verify.verify(publicKey, signature);
Performance Considerations
Module Caching Strategy
Copy
Ask AI
export class CachedModuleLoader {
private cache = new Map<string, CacheEntry>();
private maxCacheSize = 100; // MB
private maxCacheAge = 3600000; // 1 hour
async loadModule(name: string, version: string): Promise<any> {
const cacheKey = `${name}@${version}`;
const cached = this.cache.get(cacheKey);
// Check cache validity
if (cached && this.isCacheValid(cached)) {
cached.lastAccess = Date.now();
return cached.module;
}
// Load module
const module = await this.loadFromDisk(name, version);
// Update cache
this.addToCache(cacheKey, module);
// Cleanup if needed
if (this.getCacheSize() > this.maxCacheSize) {
this.evictLRU();
}
return module;
}
private isCacheValid(entry: CacheEntry): boolean {
const age = Date.now() - entry.created;
return age < this.maxCacheAge;
}
private evictLRU(): void {
let oldest: string | null = null;
let oldestTime = Date.now();
for (const [key, entry] of this.cache) {
if (entry.lastAccess < oldestTime) {
oldest = key;
oldestTime = entry.lastAccess;
}
}
if (oldest) {
this.cache.delete(oldest);
}
}
}
interface CacheEntry {
module: any;
created: number;
lastAccess: number;
size: number;
}
Next Steps
With dynamic credentials and module loading mastered, letβs explore error handling and retry strategies:Error Handling & Retry Logic
Build resilient nodes with comprehensive error handling and retry strategies