- 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.
437 lines
14 KiB
Markdown
437 lines
14 KiB
Markdown
# 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:**
|
|
|
|
1. **Salesforce Quick Action** calls `POST /orders/{sfOrderId}/fulfill`
|
|
2. **Portal BFF** processes the provisioning request
|
|
3. **Optional: Portal → Salesforce** status updates via webhook
|
|
|
|
### Secure Salesforce Quick Action Setup
|
|
|
|
**In Salesforce:**
|
|
|
|
1. **Named Credential Configuration**
|
|
```xml
|
|
<!-- 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>
|
|
```
|
|
|
|
2. **Apex Class for Secure Webhook Calls**
|
|
```apex
|
|
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:
|
|
|
|
```typescript
|
|
// 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:**
|
|
|
|
```typescript
|
|
@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:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
@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
|
|
|
|
```typescript
|
|
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
|
|
```bash
|
|
# 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
|
|
```xml
|
|
<!-- 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.
|