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

691 lines
19 KiB
Markdown

# 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:
```bash
# 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:
```bash
# 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:
```python
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:
```python
# 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
## 4. Basic Authentication (Not Recommended)
### How it works:
```bash
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 |
## Recommended Security Architecture
### For Web Applications:
```javascript
// Use session cookies with CSRF tokens
fetch('/api/resource/Customer', {
credentials: 'include',
headers: {
'X-CSRF-Token': getCsrfToken()
}
})
```
### For Mobile Applications:
```swift
// Use API tokens with secure storage
let headers = [
"Authorization": "token \(secureStorage.getApiKey()):\(secureStorage.getApiSecret())"
]
```
### For Server-to-Server:
```python
# 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:
```javascript
// 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
# 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
```python
# In ERPNext site_config.json
{
"api_ip_whitelist": ["192.168.1.0/24", "10.0.0.0/8"]
}
```
### 3. HTTPS Configuration
```nginx
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
```python
# 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
```python
# 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
```python
"""
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)
```javascript
/**
* 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)
```javascript
/**
* 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)
```bash
# 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
```yaml
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.