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.
Traditional n8n nodes have static credential types. Dynamic credentials enable:
  • 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

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.
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.
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

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

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

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

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

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

  1. Input Validation
    // Always validate module names
    const validModulePattern = /^[@a-z0-9][\w.-]*$/i;
    if (!validModulePattern.test(moduleName)) {
      throw new Error('Invalid module name');
    }
    
  2. Use Allowlists
    const allowedModules = new Set(['lodash', 'moment', 'axios']);
    if (!allowedModules.has(moduleName)) {
      throw new Error('Module not allowed');
    }
    
  3. Sandbox Execution
    const vm = require('vm');
    const sandbox = { /* limited context */ };
    vm.runInContext(code, sandbox, { timeout: 5000 });
    
  4. Version Pinning
    // Always use exact versions in production
    const moduleSpec = `${name}@${exactVersion}`;
    
  5. Signature Verification
    const crypto = require('crypto');
    const verify = crypto.createVerify('RSA-SHA256');
    verify.update(moduleContent);
    const isValid = verify.verify(publicKey, signature);
    

Performance Considerations

Module Caching Strategy

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