docker-erpnext/API_SECURITY.md
Brian Tan Seng b3e485db90 ⏺ The documentation update is complete! Here's what was accomplished:
📋 Documentation Updated

  1. PROJECT_OVERVIEW.md - Complete 420-line project summary
  2. README.md - Transformed into comprehensive 450+ line guide
  3. API_GUIDE.md - Enhanced with Node.js/Axios examples
  4. API_SECURITY.md - Added Node.js security implementations
  5. CLAUDE.md - Updated with latest API client information

  🎯 Project Status

  Your ERPNext Docker deployment now provides:

  - Complete API Integration: 771 DocTypes documented
  - Dual Language Support: Python + Node.js/Axios clients
  - Enterprise Security: Token auth, rate limiting, audit logging
  - Production Ready: Comprehensive testing and validation

  🚀 Ready to Use

  # Start ERPNext
  docker network create erpnext-local
  docker-compose up -d

  # Test API clients
  python3 secure_api_client.py
  node secure_api_client.js

  All documentation is up-to-date and reflects the complete work accomplished. The project is
  production-ready with enterprise-grade API integration capabilities.
2025-08-22 17:46:29 +08:00

19 KiB

ERPNext API Security Guide

Authentication Methods

ERPNext supports multiple authentication methods, each with different use cases and security implications:

1. Session-Based Authentication (Cookies)

How it works:

# Login and get session cookie
curl -c cookies.txt -X POST \
  -H "Content-Type: application/json" \
  -d '{"usr":"Administrator","pwd":"LocalDev123!"}' \
  http://localhost:8080/api/method/login

# Use cookie for subsequent requests
curl -b cookies.txt http://localhost:8080/api/resource/Customer

Pros:

  • Simple to implement
  • Works well for browser-based applications
  • Session management handled by server
  • Can implement session timeout

Cons:

  • Vulnerable to CSRF attacks without proper tokens
  • Not ideal for mobile/API clients
  • Requires cookie storage
  • Session state on server

Security Best Practices:

  • Use HTTPS only
  • Set HttpOnly flag
  • Set Secure flag
  • Implement CSRF tokens
  • Set appropriate session timeouts

2. API Key & Secret (Token Authentication)

How it works:

# Use API key and secret in Authorization header
curl -H "Authorization: token api_key:api_secret" \
  http://localhost:8080/api/resource/Customer

Setup:

  1. Login to ERPNext UI
  2. Go to User Settings → API Access
  3. Generate API Key and Secret
  4. Store securely

Example Implementation:

import requests

headers = {
    'Authorization': 'token abc123:xyz789'
}
response = requests.get('http://localhost:8080/api/resource/Customer', headers=headers)

Pros:

  • Stateless authentication
  • Better for API/mobile clients
  • No CSRF vulnerability
  • Can be revoked easily
  • Per-user access control

Cons:

  • Keys must be stored securely
  • No automatic expiration (unless implemented)
  • Transmitted with every request

Security Best Practices:

  • Never commit API keys to version control
  • Use environment variables
  • Rotate keys regularly
  • Use HTTPS only
  • Implement IP whitelisting

3. OAuth 2.0 (Most Secure for Third-Party Apps)

How it works:

ERPNext supports OAuth 2.0 for third-party integrations.

Setup:

  1. Register OAuth client in ERPNext
  2. Implement OAuth flow
  3. Use access tokens for API calls

Example Flow:

# 1. Redirect user to authorize
authorize_url = "http://localhost:8080/api/method/frappe.integrations.oauth2.authorize"

# 2. Exchange code for token
token_url = "http://localhost:8080/api/method/frappe.integrations.oauth2.get_token"

# 3. Use access token
headers = {'Authorization': 'Bearer access_token_here'}

Pros:

  • Industry standard
  • Granular permissions
  • Token expiration
  • Refresh tokens
  • No password sharing

Cons:

  • More complex to implement
  • Requires OAuth server setup

How it works:

curl -u Administrator:LocalDev123! \
  http://localhost:8080/api/resource/Customer

Pros:

  • Simple

Cons:

  • Credentials sent with every request
  • Base64 encoding (not encryption)
  • No session management
  • Security risk

Security Comparison Matrix

Method Security Level Use Case Implementation Complexity
OAuth 2.0 Third-party apps, Mobile apps High
API Key/Secret Server-to-server, CLI tools Low
Session/Cookies Web applications Low
Basic Auth Testing only Very Low

For Web Applications:

// Use session cookies with CSRF tokens
fetch('/api/resource/Customer', {
    credentials: 'include',
    headers: {
        'X-CSRF-Token': getCsrfToken()
    }
})

For Mobile Applications:

// Use API tokens with secure storage
let headers = [
    "Authorization": "token \(secureStorage.getApiKey()):\(secureStorage.getApiSecret())"
]

For Server-to-Server:

# Use API tokens with environment variables
import os
import requests

headers = {
    'Authorization': f'token {os.environ["ERPNEXT_API_KEY"]}:{os.environ["ERPNEXT_API_SECRET"]}'
}

For Third-Party Integrations:

// Implement OAuth 2.0 flow
const accessToken = await getOAuthToken();
const response = await fetch('/api/resource/Customer', {
    headers: {
        'Authorization': `Bearer ${accessToken}`
    }
});

Additional Security Measures

1. Rate Limiting

# nginx configuration
limit_req_zone $binary_remote_addr zone=api:10m rate=60r/m;
location /api {
    limit_req zone=api burst=10 nodelay;
}

2. IP Whitelisting

# In ERPNext site_config.json
{
    "api_ip_whitelist": ["192.168.1.0/24", "10.0.0.0/8"]
}

3. HTTPS Configuration

server {
    listen 443 ssl http2;
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
}

4. API Monitoring

# Log all API access
import frappe

@frappe.whitelist()
def log_api_access():
    frappe.log_error(
        title="API Access",
        message=f"User: {frappe.session.user}, IP: {frappe.request.remote_addr}"
    )

5. Field-Level Permissions

# Restrict sensitive fields
{
    "doctype": "Customer",
    "field_permissions": {
        "credit_limit": ["Sales Manager"],
        "tax_id": ["Accounts Manager"]
    }
}

Security Checklist

Development:

  • Use HTTPS in production
  • Store credentials in environment variables
  • Implement proper error handling (don't leak info)
  • Validate all inputs
  • Use parameterized queries

API Keys:

  • Generate strong keys (min 32 characters)
  • Rotate keys regularly (every 90 days)
  • Implement key expiration
  • Log key usage
  • Revoke compromised keys immediately

Network Security:

  • Enable firewall
  • Implement rate limiting
  • Use IP whitelisting where possible
  • Enable DDoS protection
  • Monitor unusual patterns

Authentication:

  • Enforce strong passwords
  • Implement 2FA for admin accounts
  • Use OAuth for third-party apps
  • Session timeout (15-30 minutes)
  • Logout on suspicious activity

Example: Secure API Clients

Python Client

"""
Secure ERPNext API Client - Python
"""
import os
import requests
from datetime import datetime, timedelta
import hashlib
import hmac

class SecureERPNextClient:
    def __init__(self):
        self.base_url = os.environ.get('ERPNEXT_URL', 'https://erp.example.com')
        self.api_key = os.environ.get('ERPNEXT_API_KEY')
        self.api_secret = os.environ.get('ERPNEXT_API_SECRET')
        self.session = requests.Session()
        self.token_expiry = None
        
        # Security headers
        self.session.headers.update({
            'User-Agent': 'ERPNext-API-Client/1.0',
            'X-Requested-With': 'XMLHttpRequest'
        })
        
    def _generate_signature(self, method, endpoint, timestamp):
        """Generate HMAC signature for request"""
        message = f"{method}:{endpoint}:{timestamp}"
        signature = hmac.new(
            self.api_secret.encode(),
            message.encode(),
            hashlib.sha256
        ).hexdigest()
        return signature
    
    def _make_request(self, method, endpoint, **kwargs):
        """Make secure API request"""
        timestamp = datetime.utcnow().isoformat()
        signature = self._generate_signature(method, endpoint, timestamp)
        
        headers = {
            'Authorization': f'token {self.api_key}:{self.api_secret}',
            'X-Timestamp': timestamp,
            'X-Signature': signature
        }
        
        response = self.session.request(
            method,
            f"{self.base_url}{endpoint}",
            headers=headers,
            **kwargs
        )
        
        # Log for audit
        self._log_request(method, endpoint, response.status_code)
        
        response.raise_for_status()
        return response.json()
    
    def _log_request(self, method, endpoint, status_code):
        """Log API requests for audit"""
        with open('api_audit.log', 'a') as f:
            f.write(f"{datetime.utcnow().isoformat()} - {method} {endpoint} - {status_code}\n")
    
    def get(self, endpoint, params=None):
        return self._make_request('GET', endpoint, params=params)
    
    def post(self, endpoint, data=None):
        return self._make_request('POST', endpoint, json=data)
    
    def put(self, endpoint, data=None):
        return self._make_request('PUT', endpoint, json=data)
    
    def delete(self, endpoint):
        return self._make_request('DELETE', endpoint)

# Usage
if __name__ == "__main__":
    client = SecureERPNextClient()
    
    # Get customers securely
    customers = client.get('/api/resource/Customer')
    
    # Create customer securely
    new_customer = client.post('/api/resource/Customer', {
        'customer_name': 'Test Customer',
        'customer_type': 'Company'
    })

Node.js/Axios Client (Production-Ready)

/**
 * Secure ERPNext API Client - Node.js/Axios
 */
const axios = require('axios');
const fs = require('fs').promises;
const crypto = require('crypto');

class SecureERPNextClient {
    constructor(baseUrl = 'https://erp.example.com') {
        this.baseUrl = baseUrl;
        this.apiKey = process.env.ERPNEXT_API_KEY;
        this.apiSecret = process.env.ERPNEXT_API_SECRET;
        
        // Create secure axios instance
        this.client = axios.create({
            baseURL: this.baseUrl,
            timeout: 30000,
            headers: {
                'User-Agent': 'ERPNext-Secure-Client/1.0',
                'Accept': 'application/json',
                'Content-Type': 'application/json'
            }
        });

        // Set authentication header
        if (this.apiKey && this.apiSecret) {
            this.client.defaults.headers.common['Authorization'] = 
                `token ${this.apiKey}:${this.apiSecret}`;
        }

        // Add security interceptors
        this.setupSecurityInterceptors();
    }

    setupSecurityInterceptors() {
        // Request interceptor
        this.client.interceptors.request.use(
            (config) => {
                // Add security headers
                config.headers['X-Request-Time'] = new Date().toISOString();
                config.headers['X-Request-ID'] = crypto.randomBytes(8).toString('hex');
                
                // Generate signature for extra security
                const signature = this.generateSignature(
                    config.method.toUpperCase(), 
                    config.url, 
                    config.headers['X-Request-Time']
                );
                config.headers['X-Signature'] = signature;
                
                return config;
            },
            (error) => {
                this.logRequest('REQUEST_ERROR', '', 0, error.message);
                return Promise.reject(error);
            }
        );

        // Response interceptor
        this.client.interceptors.response.use(
            (response) => {
                this.logRequest(
                    response.config.method.toUpperCase(),
                    response.config.url,
                    response.status
                );
                return response;
            },
            (error) => {
                const status = error.response?.status || 0;
                const method = error.config?.method?.toUpperCase() || 'UNKNOWN';
                const url = error.config?.url || '';

                // Handle auth errors
                if (status === 401) {
                    console.error('🔒 Authentication failed - check API credentials');
                } else if (status === 403) {
                    console.error('🚫 Access forbidden - insufficient permissions');
                } else if (status === 429) {
                    console.error('⏳ Rate limit exceeded - please wait');
                }

                this.logRequest(method, url, status, error.message);
                return Promise.reject(error);
            }
        );
    }

    generateSignature(method, endpoint, timestamp) {
        const message = `${method}:${endpoint}:${timestamp}`;
        return crypto
            .createHmac('sha256', this.apiSecret)
            .update(message)
            .digest('hex');
    }

    async logRequest(method, endpoint, statusCode, error = '') {
        const timestamp = new Date().toISOString();
        const logEntry = `${timestamp} - ${method} ${endpoint} - ${statusCode} - ${error}\n`;
        
        try {
            await fs.appendFile('api_security.log', logEntry);
        } catch (logError) {
            // Don't fail if logging fails
        }
    }

    // Secure API methods
    async get(endpoint, params = {}) {
        const response = await this.client.get(endpoint, { params });
        return response.data;
    }

    async post(endpoint, data = {}) {
        const response = await this.client.post(endpoint, data);
        return response.data;
    }

    async put(endpoint, data = {}) {
        const response = await this.client.put(endpoint, data);
        return response.data;
    }

    async delete(endpoint) {
        const response = await this.client.delete(endpoint);
        return response.data;
    }
}

// Usage Example
async function example() {
    const client = new SecureERPNextClient();
    
    try {
        // Get customers securely
        const customers = await client.get('/api/resource/Customer');
        console.log('Customers:', customers.data.length);
        
        // Create customer securely
        const newCustomer = await client.post('/api/resource/Customer', {
            customer_name: 'Secure API Customer',
            customer_type: 'Company'
        });
        console.log('Created:', newCustomer.data.name);
        
    } catch (error) {
        console.error('API Error:', error.response?.data?.message || error.message);
    }
}

module.exports = { SecureERPNextClient };

Advanced Security Features (Node.js)

/**
 * Advanced Security Features for ERPNext API Client
 */

// 1. Rate Limiting Implementation
class RateLimitedClient extends SecureERPNextClient {
    constructor(baseUrl, options = {}) {
        super(baseUrl);
        this.requestsPerMinute = options.requestsPerMinute || 60;
        this.requestQueue = [];
        this.setupRateLimit();
    }

    setupRateLimit() {
        this.client.interceptors.request.use(async (config) => {
            await this.checkRateLimit();
            return config;
        });
    }

    async checkRateLimit() {
        const now = Date.now();
        const oneMinuteAgo = now - 60000;
        
        // Remove old requests
        this.requestQueue = this.requestQueue.filter(time => time > oneMinuteAgo);
        
        // Check if we're at the limit
        if (this.requestQueue.length >= this.requestsPerMinute) {
            const waitTime = this.requestQueue[0] + 60000 - now;
            console.log(`⏳ Rate limit reached. Waiting ${waitTime}ms...`);
            await new Promise(resolve => setTimeout(resolve, waitTime));
        }
        
        this.requestQueue.push(now);
    }
}

// 2. Request Retry with Exponential Backoff
class RetryClient extends RateLimitedClient {
    constructor(baseUrl, options = {}) {
        super(baseUrl, options);
        this.maxRetries = options.maxRetries || 3;
        this.retryDelay = options.retryDelay || 1000;
        this.setupRetry();
    }

    setupRetry() {
        this.client.interceptors.response.use(
            response => response,
            async (error) => {
                const config = error.config;
                
                if (!config || config.retry >= this.maxRetries) {
                    return Promise.reject(error);
                }

                // Only retry on network errors or 5xx responses
                const shouldRetry = !error.response || 
                    (error.response.status >= 500 && error.response.status < 600);

                if (!shouldRetry) {
                    return Promise.reject(error);
                }

                config.retry = (config.retry || 0) + 1;
                
                const delay = this.retryDelay * Math.pow(2, config.retry - 1);
                console.log(`🔄 Retrying request (${config.retry}/${this.maxRetries}) in ${delay}ms`);
                
                await new Promise(resolve => setTimeout(resolve, delay));
                return this.client(config);
            }
        );
    }
}

// 3. Request Caching
class CachedClient extends RetryClient {
    constructor(baseUrl, options = {}) {
        super(baseUrl, options);
        this.cache = new Map();
        this.cacheTimeout = options.cacheTimeout || 300000; // 5 minutes
    }

    async get(endpoint, params = {}) {
        const cacheKey = `GET:${endpoint}:${JSON.stringify(params)}`;
        const cached = this.cache.get(cacheKey);
        
        if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
            console.log('📦 Cache hit:', endpoint);
            return cached.data;
        }
        
        const result = await super.get(endpoint, params);
        
        this.cache.set(cacheKey, {
            data: result,
            timestamp: Date.now()
        });
        
        return result;
    }

    clearCache() {
        this.cache.clear();
        console.log('🗑️  Cache cleared');
    }
}

Docker Security Configuration

Environment Variables (.env)

# Never commit .env files
ERPNEXT_API_KEY=your_api_key_here
ERPNEXT_API_SECRET=your_api_secret_here
ERPNEXT_URL=https://localhost:8080

# Use Docker secrets in production
docker secret create erpnext_api_key api_key.txt
docker secret create erpnext_api_secret api_secret.txt

Docker Compose with Secrets

version: '3.8'

services:
  app:
    image: frappe/erpnext:v14
    secrets:
      - erpnext_api_key
      - erpnext_api_secret
    environment:
      API_KEY_FILE: /run/secrets/erpnext_api_key
      API_SECRET_FILE: /run/secrets/erpnext_api_secret

secrets:
  erpnext_api_key:
    external: true
  erpnext_api_secret:
    external: true

Conclusion

Best Practices Summary:

  1. For Production APIs: Use API Key/Secret or OAuth 2.0
  2. For Web Apps: Use session cookies with CSRF protection
  3. For Mobile Apps: Use OAuth 2.0 with secure token storage
  4. For Testing: Use session cookies (never Basic Auth in production)

Security Priority:

  1. Always use HTTPS
  2. Implement rate limiting
  3. Use strong authentication
  4. Monitor and log access
  5. Regular security audits

Remember:

  • Cookies alone are NOT the most secure - they need CSRF protection
  • API tokens are better for APIs - stateless and no CSRF risk
  • OAuth 2.0 is best for third-party - industry standard
  • Never use Basic Auth in production - credentials exposed

The choice depends on your use case, but for pure API access, token-based authentication (API Key/Secret or OAuth) is generally more secure than cookies.