Document Status: Operational Guide Last Updated: September 30, 2025 Source: CORE-GREAT-2C Phase 3 (TBD-SECURITY-02 Resolution)
Piper Morgan implements a graceful degradation security pattern for webhook endpoints, providing developer-friendly defaults while maintaining production security. This design enables seamless local development without sacrificing security in production environments.
Developer Experience First, Security Always
The webhook security system follows these principles:
Trigger: No signing secret configured (SLACK_SIGNING_SECRET not set)
Behavior:
if not signing_secret:
logger.warning("No Slack signing secret configured, skipping signature verification")
return True # Allow request to proceed
Characteristics:
200 OK for all requestsUse Cases:
Security Level: ⚠️ Development Only - Not suitable for production
Trigger: Signing secret configured (SLACK_SIGNING_SECRET environment variable set)
Behavior:
# Validate timestamp (prevent replay attacks)
current_time = int(time.time())
if abs(current_time - int(timestamp)) > 300:
logger.warning("Slack request timestamp too old")
return False
# Compute expected signature using HMAC-SHA256
sig_basestring = f"v0:{timestamp}:{body.decode()}"
expected_signature = "v0=" + hmac.new(
signing_secret.encode(),
sig_basestring.encode(),
hashlib.sha256
).hexdigest()
# Timing-safe comparison
return hmac.compare_digest(signature, expected_signature)
Characteristics:
401 UnauthorizedUse Cases:
Security Level: 🔒 Production Grade - Industry standard security
Location: services/integrations/slack/webhook_router.py
Method: _verify_slack_signature(self, request: Request) -> bool
Implementation (lines 442-487):
async def _verify_slack_signature(self, request: Request) -> bool:
"""
Verify Slack request signature for security.
Implements Slack's signature verification protocol:
1. Extract timestamp and signature from headers
2. Validate timestamp (prevent replay attacks)
3. Compute expected signature using HMAC-SHA256
4. Compare signatures using timing-safe comparison
Returns:
True if signature valid or development mode (no signing secret)
False if signature invalid or missing required headers
"""
try:
# Get signing secret from configuration
config = self.config_service.get_config()
signing_secret = config.signing_secret
# Development mode: Allow requests without signing secret
if not signing_secret:
logger.warning("No Slack signing secret configured, skipping signature verification")
return True # Graceful degradation
# Extract required headers
timestamp = request.headers.get("X-Slack-Request-Timestamp")
signature = request.headers.get("X-Slack-Signature")
# Validate headers present
if not timestamp or not signature:
logger.warning("Missing timestamp or signature in Slack request")
return False
# Validate timestamp (prevent replay attacks - 5 minute window)
current_time = int(time.time())
if abs(current_time - int(timestamp)) > 300:
logger.warning("Slack request timestamp too old")
return False
# Get request body
body = await request.body()
# Compute expected signature (Slack protocol)
sig_basestring = f"v0:{timestamp}:{body.decode()}"
expected_signature = (
"v0=" + hmac.new(
signing_secret.encode(),
sig_basestring.encode(),
hashlib.sha256
).hexdigest()
)
# Timing-safe comparison (prevents timing attacks)
return hmac.compare_digest(signature, expected_signature)
except Exception as e:
logger.error(f"Error verifying Slack signature: {e}")
return False
All webhook endpoints call verification before processing requests:
Endpoint: /slack/webhooks/events
Method: _handle_events_webhook (line 180)
Protection (lines 184-188):
# Verify request signature
if not await self._verify_slack_signature(request):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid request signature"
)
Status: ✅ Protected (enabled in Phase 3)
Endpoint: /slack/webhooks/interactive
Method: _handle_interactive_webhook (line 311)
Protection (lines 315-319):
# Verify request signature
if not await self._verify_slack_signature(request):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid request signature"
)
Status: ✅ Protected (previously enabled)
Endpoint: /slack/webhooks/commands
Method: _handle_commands_webhook (line 350)
Protection (lines 354-358):
# Verify request signature
if not await self._verify_slack_signature(request):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid request signature"
)
Status: ✅ Protected (previously enabled)
Protection Coverage: 100% (3/3 webhook endpoints protected)
Standard: Slack Web API specification Algorithm: HMAC with SHA-256 hash function Key Material: Slack signing secret (from workspace settings)
Process:
v0), timestamp, and request bodyX-Slack-Signature headerSecurity Benefit: Ensures request originated from Slack and wasn’t tampered with
Mechanism: Timestamp validation with tolerance window
Tolerance: 5 minutes (300 seconds)
Header: X-Slack-Request-Timestamp
Process:
Security Benefit: Prevents attackers from replaying captured valid requests
Function: hmac.compare_digest(signature, expected_signature)
Purpose: Prevent timing attacks
Process:
Security Benefit: Prevents attackers from using response time to guess valid signatures
Required Headers:
X-Slack-Request-Timestamp - Request timestamp (Unix epoch)X-Slack-Signature - HMAC-SHA256 signature with version prefixValidation:
v0=)Security Benefit: Ensures request has required security metadata
Philosophy: Fail closed (reject on error)
Behavior:
except Exception as e:
logger.error(f"Error verifying Slack signature: {e}")
return False # Reject request on any error
Security Benefit: Errors don’t create security vulnerabilities
Warnings:
Errors:
Security Benefit: Visibility into security events for monitoring and debugging
No configuration required - webhook endpoints work immediately.
Behavior: All requests accepted (graceful degradation)
Required: Set SLACK_SIGNING_SECRET environment variable
How to Get Signing Secret:
Configuration:
# Set environment variable
export SLACK_SIGNING_SECRET=your_signing_secret_here
# Or in .env file
SLACK_SIGNING_SECRET=your_signing_secret_here
# Restart server to activate
./stop-piper.sh && ./start-piper.sh
Verification: Check logs for absence of warnings:
# Should NOT see this in production:
WARNING: No Slack signing secret configured, skipping signature verification
Test: Webhook accepts requests without signatures
# Send request without signature
curl -X POST http://localhost:8001/slack/webhooks/events \
-H "Content-Type: application/json" \
-d '{"type": "url_verification", "challenge": "test123"}'
# Expected: 200 OK (development mode allows)
# Expected log: WARNING: No Slack signing secret configured
Result: ✅ Works in development (graceful degradation)
Test 1: Valid signature accepted
# Generate valid signature using signing secret
TIMESTAMP=$(date +%s)
BODY='{"type":"url_verification","challenge":"test123"}'
SIG_BASE="v0:${TIMESTAMP}:${BODY}"
SIGNATURE="v0=$(echo -n "$SIG_BASE" | openssl dgst -sha256 -hmac "$SLACK_SIGNING_SECRET" | cut -d' ' -f2)"
# Send request with valid signature
curl -X POST https://production.app/slack/webhooks/events \
-H "Content-Type: application/json" \
-H "X-Slack-Request-Timestamp: $TIMESTAMP" \
-H "X-Slack-Signature: $SIGNATURE" \
-d "$BODY"
# Expected: 200 OK (valid signature)
Test 2: Invalid signature rejected
# Send request with invalid signature
curl -X POST https://production.app/slack/webhooks/events \
-H "Content-Type: application/json" \
-H "X-Slack-Request-Timestamp: $(date +%s)" \
-H "X-Slack-Signature: v0=invalid_signature" \
-d '{"type":"url_verification","challenge":"test123"}'
# Expected: 401 Unauthorized (invalid signature)
Test 3: Old timestamp rejected (replay attack)
# Send request with old timestamp (> 5 minutes ago)
OLD_TIMESTAMP=$(($(date +%s) - 400))
curl -X POST https://production.app/slack/webhooks/events \
-H "Content-Type: application/json" \
-H "X-Slack-Request-Timestamp: $OLD_TIMESTAMP" \
-H "X-Slack-Signature: v0=some_signature" \
-d '{"type":"url_verification","challenge":"test123"}'
# Expected: 401 Unauthorized (timestamp too old)
Issue: Events webhook (/slack/webhooks/events) had signature verification disabled
Root Cause: Lines 184-189 in webhook_router.py were commented out with note “disabled for integration testing”
Resolution:
Impact:
Files Modified:
services/integrations/slack/webhook_router.py (lines 184-188)Verification:
_verify_slack_signatureCause: Invalid signature verification
Solutions:
SLACK_SIGNING_SECRET is set correctlyCause: No signing secret configured (development mode)
Solutions:
SLACK_SIGNING_SECRET environment variableCause: Timestamp validation failing (clock skew)
Solutions:
Standards Followed:
Security Properties:
Piper Morgan’s webhook security architecture successfully balances developer experience with production security through graceful degradation. The system:
Key Insight: Security doesn’t have to hurt developer experience when designed thoughtfully.
See Also:
Maintained by: Piper Morgan Core Team
Questions: Create a GitHub issue with label security