- Updated PLESK_DEPLOYMENT.md to include new Salesforce credentials and webhook security configurations. - Refactored order fulfillment controller to streamline the process and improve readability. - Introduced EnhancedWebhookSignatureGuard for improved HMAC signature validation and nonce management. - Updated various documentation files to reflect changes in endpoint naming from `/provision` to `/fulfill` for clarity and consistency. - Enhanced Redis integration for nonce storage to prevent replay attacks. - Removed deprecated WebhookSignatureGuard in favor of the new enhanced guard.
14 KiB
Salesforce-to-Portal Security Integration Guide
Overview
This guide outlines secure patterns for Salesforce-to-Portal communication specifically for the order provisioning workflow. Based on your architecture, this focuses on order status updates, not invoice handling.
Order Provisioning Flow
Portal Customer → Places Order → Salesforce Order (Pending Review)
↓
Salesforce Operator → Reviews → Clicks "Provision in WHMCS" Quick Action
↓
Salesforce → Calls Portal BFF → `/orders/{sfOrderId}/fulfill`
↓
Portal BFF → Provisions in WHMCS → Updates Salesforce Order Status
↓
Portal → Polls Order Status → Shows Customer Updates
1. Secure Order Provisioning Communication
Primary Method: Direct HTTPS Webhook (Recommended for Order Flow)
Based on your architecture, the order provisioning flow uses direct HTTPS calls from Salesforce to your portal BFF. Here's how to secure this:
Salesforce → Portal BFF Flow:
- Salesforce Quick Action calls
POST /orders/{sfOrderId}/fulfill - Portal BFF processes the provisioning request
- Optional: Portal → Salesforce status updates via webhook
Secure Salesforce Quick Action Setup
In Salesforce:
- Named Credential Configuration
<!-- Named Credential: Portal_BFF -->
<NamedCredential>
<fullName>Portal_BFF</fullName>
<endpoint>https://your-portal-api.com</endpoint>
<principalType>Anonymous</principalType>
<protocol>HttpsOnly</protocol>
<generateAuthorizationHeader>false</generateAuthorizationHeader>
</NamedCredential>
- Apex Class for Secure Webhook Calls
public class PortalWebhookService {
private static final String WEBHOOK_SECRET = '{!$Credential.Portal_Webhook.Password}';
@future(callout=true)
public static void provisionOrder(String orderId) {
try {
// Prepare secure payload
Map<String, Object> payload = new Map<String, Object>{
'orderId' => orderId,
'timestamp' => System.now().format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\''),
'nonce' => generateNonce()
};
// Create HMAC signature
String jsonPayload = JSON.serialize(payload);
String signature = generateHMACSignature(jsonPayload, WEBHOOK_SECRET);
// Make secure HTTP call
HttpRequest req = new HttpRequest();
req.setEndpoint('callout:Portal_BFF/orders/' + orderId + '/fulfill');
req.setMethod('POST');
req.setHeader('Content-Type', 'application/json');
req.setHeader('X-SF-Signature', signature);
req.setHeader('X-SF-Timestamp', payload.get('timestamp').toString());
req.setHeader('X-SF-Nonce', payload.get('nonce').toString());
req.setHeader('Idempotency-Key', generateIdempotencyKey(orderId));
req.setBody(jsonPayload);
req.setTimeout(30000); // 30 second timeout
Http http = new Http();
HttpResponse res = http.send(req);
// Handle response
handleProvisioningResponse(orderId, res);
} catch (Exception e) {
// Log error and update order status
System.debug('Provisioning failed for order ' + orderId + ': ' + e.getMessage());
updateOrderProvisioningStatus(orderId, 'Failed', e.getMessage());
}
}
private static String generateHMACSignature(String data, String key) {
Blob hmacData = Crypto.generateMac('HmacSHA256', Blob.valueOf(data), Blob.valueOf(key));
return EncodingUtil.convertToHex(hmacData);
}
private static String generateNonce() {
return EncodingUtil.convertToHex(Crypto.generateAesKey(128)).substring(0, 16);
}
private static String generateIdempotencyKey(String orderId) {
return 'provision_' + orderId + '_' + System.now().getTime();
}
}
### Optional: Portal → Salesforce Status Updates
If you want the portal to send status updates back to Salesforce (e.g., when provisioning completes), you can set up a reverse webhook:
**Portal BFF → Salesforce Webhook Endpoint:**
```typescript
// In your Portal BFF
export class SalesforceStatusUpdateService {
async updateOrderStatus(orderId: string, status: string, details?: any) {
const payload = {
orderId,
status,
timestamp: new Date().toISOString(),
details: this.sanitizeDetails(details)
};
// Send to Salesforce webhook endpoint
await this.sendToSalesforce('/webhook/order-status', payload);
}
}
2. Portal BFF Security Implementation
Enhanced Order Provisioning Endpoint
Your portal BFF should implement the /orders/{sfOrderId}/fulfill endpoint with these security measures:
// Enhanced order fulfillment endpoint
@Post('orders/:sfOrderId/fulfill')
@UseGuards(EnhancedWebhookSignatureGuard)
async fulfillOrder(
@Param('sfOrderId') sfOrderId: string,
@Body() payload: ProvisionOrderRequest,
@Headers('idempotency-key') idempotencyKey: string
) {
// Your existing fulfillment logic
return await this.orderFulfillmentService.fulfillOrder(sfOrderId, payload, idempotencyKey);
}
Enhanced Webhook Security Implementation:
@Injectable()
export class EnhancedWebhookSignatureGuard implements CanActivate {
constructor(private configService: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>();
// 1. Verify HMAC signature (existing)
this.verifyHmacSignature(request);
// 2. Verify timestamp (prevent replay attacks)
this.verifyTimestamp(request);
// 3. Verify nonce (prevent duplicate processing)
this.verifyNonce(request);
// 4. Verify source IP (if using IP allowlisting)
this.verifySourceIp(request);
return true;
}
private verifyTimestamp(request: Request): void {
const timestamp = request.headers['x-sf-timestamp'] as string;
if (!timestamp) {
throw new UnauthorizedException('Timestamp required');
}
const requestTime = new Date(timestamp).getTime();
const now = Date.now();
const maxAge = 5 * 60 * 1000; // 5 minutes
if (Math.abs(now - requestTime) > maxAge) {
throw new UnauthorizedException('Request too old');
}
}
private verifyNonce(request: Request): void {
const nonce = request.headers['x-sf-nonce'] as string;
if (!nonce) {
throw new UnauthorizedException('Nonce required');
}
// Check if nonce was already used (implement nonce store)
// This prevents replay attacks
}
}
2. Outbound Security: Portal → Salesforce
Current Implementation (Already Secure ✅)
Your existing JWT-based authentication is excellent:
// Your current pattern in salesforce-connection.service.ts
// Uses private key JWT authentication - industry standard
Enhanced Patterns for Sensitive Operations
For highly sensitive operations, consider adding:
@Injectable()
export class SecureSalesforceService {
async createSensitiveRecord(data: SensitiveData, idempotencyKey: string) {
// 1. Encrypt sensitive fields before sending
const encryptedData = this.encryptSensitiveFields(data);
// 2. Add idempotency protection
const headers = {
'Idempotency-Key': idempotencyKey,
'X-Request-ID': uuidv4(),
};
// 3. Use your existing secure connection
return await this.salesforceConnection.create(encryptedData, headers);
}
private encryptSensitiveFields(data: any): any {
// Encrypt PII fields before transmission
const sensitiveFields = ['ssn', 'creditCard', 'personalId'];
// Implementation depends on your encryption strategy
}
}
3. Data Protection Guidelines
Sensitive Data Handling
// Example: Secure order processing
export class SecureOrderService {
async processOrderApproval(orderData: OrderApprovalData) {
// 1. Validate customer permissions
await this.validateCustomerAccess(orderData.customerNumber);
// 2. Sanitize data for logging
const sanitizedData = this.sanitizeForLogging(orderData);
this.logger.log('Processing order approval', sanitizedData);
// 3. Process with minimal data exposure
const result = await this.processOrder(orderData);
// 4. Audit trail without sensitive data
await this.createAuditLog({
action: 'order_approved',
customerNumber: orderData.customerNumber,
orderId: orderData.orderId,
timestamp: new Date(),
// No sensitive payment or personal data
});
return result;
}
private sanitizeForLogging(data: any): any {
// Remove or mask sensitive fields for logging
const { creditCard, ssn, ...safeData } = data;
return {
...safeData,
creditCard: creditCard ? '****' + creditCard.slice(-4) : undefined,
ssn: ssn ? '***-**-' + ssn.slice(-4) : undefined,
};
}
}
Field-Level Security
// Implement field-level encryption for highly sensitive data
export class FieldEncryptionService {
private readonly algorithm = 'aes-256-gcm';
private readonly keyDerivation = 'pbkdf2';
async encryptField(value: string, fieldType: string): Promise<EncryptedField> {
const key = await this.deriveKey(fieldType);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipher(this.algorithm, key);
let encrypted = cipher.update(value, 'utf8', 'hex');
encrypted += cipher.final('hex');
return {
value: encrypted,
iv: iv.toString('hex'),
tag: cipher.getAuthTag().toString('hex'),
};
}
async decryptField(encryptedField: EncryptedField, fieldType: string): Promise<string> {
const key = await this.deriveKey(fieldType);
const decipher = crypto.createDecipher(this.algorithm, key);
decipher.setAuthTag(Buffer.from(encryptedField.tag, 'hex'));
let decrypted = decipher.update(encryptedField.value, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
}
4. Implementation Checklist
Salesforce Setup
- Create Platform Events for portal notifications
- Set up Named Credentials for portal webhook calls
- Configure IP allowlisting for portal endpoints
- Implement HMAC signing in Apex
- Create audit trails for all portal communications
Portal Setup
- Enhance webhook signature verification
- Implement timestamp and nonce validation
- Add IP allowlisting for Salesforce
- Create encrypted payload handling
- Implement idempotency protection
Security Measures
- Rotate webhook secrets regularly
- Monitor for suspicious webhook activity
- Implement rate limiting per customer
- Add comprehensive audit logging
- Test disaster recovery procedures
5. Monitoring and Alerting
@Injectable()
export class SecurityMonitoringService {
async monitorWebhookSecurity(request: Request, response: any) {
const metrics = {
sourceIp: request.ip,
userAgent: request.headers['user-agent'],
timestamp: new Date(),
success: response.success,
processingTime: response.processingTime,
};
// Alert on suspicious patterns
if (this.detectSuspiciousActivity(metrics)) {
await this.sendSecurityAlert(metrics);
}
// Log for audit
this.logger.log('Webhook security metrics', metrics);
}
private detectSuspiciousActivity(metrics: any): boolean {
// Implement your security detection logic
// - Too many requests from same IP
// - Unusual timing patterns
// - Failed authentication attempts
return false;
}
}
6. Testing Security
describe('Webhook Security', () => {
it('should reject webhooks without valid HMAC signature', async () => {
const invalidPayload = { data: 'test' };
const response = await request(app)
.post('/webhooks/salesforce')
.send(invalidPayload)
.expect(401);
expect(response.body.message).toContain('Invalid webhook signature');
});
it('should reject old timestamps', async () => {
const oldTimestamp = new Date(Date.now() - 10 * 60 * 1000); // 10 minutes ago
const payload = { data: 'test' };
const signature = generateHmacSignature(payload);
const response = await request(app)
.post('/webhooks/salesforce')
.set('X-SF-Signature', signature)
.set('X-SF-Timestamp', oldTimestamp.toISOString())
.send(payload)
.expect(401);
});
});
7. Production Deployment
Environment Variables
# Webhook Security
SF_WEBHOOK_SECRET=your_256_bit_secret_key_here
SF_WEBHOOK_IP_ALLOWLIST=13.108.0.0/14,204.14.232.0/23
WEBHOOK_TIMESTAMP_TOLERANCE=300000 # 5 minutes in ms
# Encryption
FIELD_ENCRYPTION_KEY=your_field_encryption_master_key
ENCRYPTION_KEY_ROTATION_DAYS=90
# Monitoring
SECURITY_ALERT_WEBHOOK=https://your-monitoring-service.com/alerts
AUDIT_LOG_RETENTION_DAYS=2555 # 7 years for compliance
Salesforce Named Credential Setup
<!-- Named Credential: Portal_Webhook -->
<NamedCredential>
<fullName>Portal_Webhook</fullName>
<endpoint>https://your-portal-api.com</endpoint>
<principalType>Anonymous</principalType>
<protocol>HttpsOnly</protocol>
<generateAuthorizationHeader>false</generateAuthorizationHeader>
</NamedCredential>
This guide provides a comprehensive, production-ready approach to secure Salesforce-Portal integration that builds on your existing security infrastructure while adding enterprise-grade protection for sensitive data transmission.