- 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.
11 KiB
Salesforce-to-Portal Order Communication Guide
Overview
This guide focuses specifically on secure communication between Salesforce and your Portal for order provisioning. This is NOT about invoices or billing - it's about the order approval and provisioning workflow.
The Order Flow
1. Customer places order → Portal creates Salesforce Order (Status: "Pending Review")
2. Salesforce operator reviews → Clicks "Provision in WHMCS" Quick Action
3. Salesforce calls Portal BFF → POST /orders/{sfOrderId}/fulfill
4. Portal BFF provisions in WHMCS → Updates Salesforce Order status
5. Customer sees updated status in Portal
1. Salesforce → Portal (Order Provisioning)
Current Implementation ✅
Your existing architecture already handles this securely via the Quick Action that calls your BFF endpoint:
- Endpoint:
POST /orders/{sfOrderId}/fulfill - Authentication: Named Credentials + HMAC signature
- Security: IP allowlisting, idempotency keys, signed headers
Enhanced Security Implementation
Use your existing EnhancedWebhookSignatureGuard for the provisioning endpoint:
// apps/bff/src/orders/controllers/order-fulfillment.controller.ts
@Post(':sfOrderId/fulfill')
@UseGuards(EnhancedWebhookSignatureGuard)
@ApiHeader({ name: "X-SF-Signature", description: "Salesforce HMAC signature" })
@ApiHeader({ name: "X-SF-Timestamp", description: "Request timestamp" })
@ApiHeader({ name: "X-SF-Nonce", description: "Unique nonce" })
@ApiHeader({ name: "Idempotency-Key", description: "Idempotency key" })
async fulfillOrder(
@Param('sfOrderId') sfOrderId: string,
@Body() payload: { orderId: string; timestamp: string; nonce: string },
@Headers('idempotency-key') idempotencyKey: string
) {
return await this.orderFulfillmentService.fulfillOrder(sfOrderId, payload, idempotencyKey);
}
Salesforce Apex Implementation
public class OrderProvisioningService {
private static final String WEBHOOK_SECRET = '{!$Credential.Portal_Webhook.Password}';
@future(callout=true)
public static void provisionOrder(String orderId) {
try {
// Create 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()
};
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', 'provision_' + orderId + '_' + System.now().getTime());
req.setBody(jsonPayload);
req.setTimeout(30000);
Http http = new Http();
HttpResponse res = http.send(req);
handleProvisioningResponse(orderId, res);
} catch (Exception e) {
updateOrderStatus(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 void handleProvisioningResponse(String orderId, HttpResponse res) {
if (res.getStatusCode() == 200) {
updateOrderStatus(orderId, 'Provisioned', null);
} else {
updateOrderStatus(orderId, 'Failed', 'HTTP ' + res.getStatusCode());
}
}
private static void updateOrderStatus(String orderId, String status, String errorMessage) {
Order ord = [SELECT Id FROM Order WHERE Id = :orderId LIMIT 1];
ord.Provisioning_Status__c = status;
if (errorMessage != null) {
ord.Provisioning_Error_Message__c = errorMessage.left(255); // Truncate if needed
}
update ord;
}
}
2. Optional: Portal → Salesforce (Status Updates)
If you want to send status updates back to Salesforce during provisioning, you can implement a reverse webhook:
Portal BFF Implementation
// apps/bff/src/vendors/salesforce/services/order-status-update.service.ts
@Injectable()
export class OrderStatusUpdateService {
constructor(
private salesforceConnection: SalesforceConnection,
@Inject(Logger) private logger: Logger
) {}
async updateOrderStatus(
sfOrderId: string,
status: 'Activating' | 'Provisioned' | 'Failed',
details?: {
whmcsOrderId?: string;
errorCode?: string;
errorMessage?: string;
}
) {
try {
const updateData: any = {
Id: sfOrderId,
Provisioning_Status__c: status,
Last_Provisioning_At__c: new Date().toISOString(),
};
if (details?.whmcsOrderId) {
updateData.WHMCS_Order_ID__c = details.whmcsOrderId;
}
if (status === 'Failed' && details?.errorCode) {
updateData.Provisioning_Error_Code__c = details.errorCode;
updateData.Provisioning_Error_Message__c = details.errorMessage?.substring(0, 255);
}
await this.salesforceConnection.sobject('Order').update(updateData);
this.logger.log('Order status updated in Salesforce', {
sfOrderId,
status,
whmcsOrderId: details?.whmcsOrderId,
});
} catch (error) {
this.logger.error('Failed to update order status in Salesforce', {
sfOrderId,
status,
error: error instanceof Error ? error.message : String(error),
});
// Don't throw - this is a non-critical update
}
}
}
Usage in Order Orchestrator
// In your existing OrderOrchestrator service
async fulfillOrder(sfOrderId: string, payload: any, idempotencyKey: string) {
try {
// Update status to "Activating"
await this.orderStatusUpdateService.updateOrderStatus(sfOrderId, 'Activating');
// Your existing provisioning logic...
const whmcsOrderId = await this.provisionInWhmcs(sfOrderId, payload);
// Update status to "Provisioned" with WHMCS order ID
await this.orderStatusUpdateService.updateOrderStatus(sfOrderId, 'Provisioned', {
whmcsOrderId: whmcsOrderId.toString(),
});
return { success: true, whmcsOrderId };
} catch (error) {
// Update status to "Failed" with error details
await this.orderStatusUpdateService.updateOrderStatus(sfOrderId, 'Failed', {
errorCode: 'PROVISIONING_ERROR',
errorMessage: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
3. Security Configuration
Environment Variables
# Salesforce 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
# Monitoring
SECURITY_ALERT_WEBHOOK=https://your-monitoring-service.com/alerts
Salesforce Named Credential
<!-- 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>
<!-- Named Credential: Portal_Webhook (for the secret) -->
<NamedCredential>
<fullName>Portal_Webhook</fullName>
<endpoint>https://your-portal-api.com</endpoint>
<principalType>NamedPrincipal</principalType>
<namedCredentialType>Legacy</namedCredentialType>
<password>your_256_bit_secret_key_here</password>
<username>webhook</username>
</NamedCredential>
4. Customer Experience
Portal UI Polling
The portal should poll for order status updates:
// In your Portal UI
export function useOrderStatus(sfOrderId: string) {
const [status, setStatus] = useState<OrderStatus>('Pending Review');
useEffect(() => {
const pollStatus = async () => {
try {
const response = await fetch(`/api/orders/${sfOrderId}`);
const data = await response.json();
setStatus(data.status);
// Stop polling when order is complete
if (['Provisioned', 'Failed'].includes(data.status)) {
clearInterval(interval);
}
} catch (error) {
console.error('Failed to fetch order status:', error);
}
};
const interval = setInterval(pollStatus, 5000); // Poll every 5 seconds
pollStatus(); // Initial fetch
return () => clearInterval(interval);
}, [sfOrderId]);
return status;
}
5. Monitoring and Alerting
Key Metrics to Monitor
- Provisioning Success Rate: Track successful vs failed provisioning attempts
- Provisioning Latency: Time from Quick Action to completion
- WHMCS API Errors: Monitor WHMCS integration health
- Webhook Security Events: Failed signature validations, old timestamps
Alert Conditions
// Example monitoring service
@Injectable()
export class OrderProvisioningMonitoringService {
async recordProvisioningAttempt(sfOrderId: string, success: boolean, duration: number) {
// Record metrics
this.metricsService.increment('order.provisioning.attempts', {
success: success.toString(),
});
this.metricsService.histogram('order.provisioning.duration', duration);
// Alert on high failure rate
const recentFailureRate = await this.getRecentFailureRate();
if (recentFailureRate > 0.1) { // 10% failure rate
await this.alertingService.sendAlert('High order provisioning failure rate');
}
}
}
6. Testing
Security Testing
describe('Order Provisioning Security', () => {
it('should reject requests without valid HMAC signature', async () => {
const response = await request(app)
.post('/orders/test-order-id/fulfill')
.send({ orderId: 'test-order-id' })
.expect(401);
});
it('should reject requests with old timestamps', async () => {
const oldTimestamp = new Date(Date.now() - 10 * 60 * 1000).toISOString();
const payload = { orderId: 'test-order-id', timestamp: oldTimestamp };
const signature = generateHmacSignature(JSON.stringify(payload));
const response = await request(app)
.post('/orders/test-order-id/fulfill')
.set('X-SF-Signature', signature)
.set('X-SF-Timestamp', oldTimestamp)
.send(payload)
.expect(401);
});
});
Summary
This focused approach ensures secure communication specifically for your order provisioning workflow:
- Salesforce Quick Action → Secure HTTPS call to Portal BFF
- Portal BFF → Processes order, provisions in WHMCS
- Optional: Portal sends status updates back to Salesforce
- Customer → Sees real-time order status in Portal UI
The security is handled by your existing infrastructure with enhanced webhook signature validation, making it production-ready and secure memory:6689308.