2026-04-20 18:01:48 -04:00
|
|
|
import base64
|
2026-04-20 17:56:00 -04:00
|
|
|
import hashlib
|
|
|
|
|
import hmac
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
import boto3
|
|
|
|
|
|
|
|
|
|
lambda_client = boto3.client("lambda")
|
2026-04-20 18:01:48 -04:00
|
|
|
secrets_client = boto3.client("secretsmanager")
|
2026-04-20 17:56:00 -04:00
|
|
|
|
2026-04-20 18:01:48 -04:00
|
|
|
SLACK_SIGNING_SECRET_ARN = os.environ["SLACK_SIGNING_SECRET_ARN"]
|
2026-04-20 17:56:00 -04:00
|
|
|
PROCESSOR_FUNCTION_NAME = os.environ["PROCESSOR_FUNCTION_NAME"]
|
|
|
|
|
|
2026-04-20 18:01:48 -04:00
|
|
|
_cached_signing_secret: str | None = None
|
|
|
|
|
|
2026-04-20 17:56:00 -04:00
|
|
|
|
|
|
|
|
def handler(event, context):
|
|
|
|
|
body = event.get("body") or ""
|
|
|
|
|
if event.get("isBase64Encoded"):
|
|
|
|
|
body = base64.b64decode(body).decode("utf-8")
|
|
|
|
|
|
|
|
|
|
headers = {k.lower(): v for k, v in (event.get("headers") or {}).items()}
|
|
|
|
|
timestamp = headers.get("x-slack-request-timestamp", "")
|
|
|
|
|
signature = headers.get("x-slack-signature", "")
|
|
|
|
|
|
|
|
|
|
if not verify_signature(body, timestamp, signature):
|
|
|
|
|
return {"statusCode": 401, "body": "unauthorized"}
|
|
|
|
|
|
|
|
|
|
payload = json.loads(body)
|
|
|
|
|
|
|
|
|
|
if payload.get("type") == "url_verification":
|
|
|
|
|
return {
|
|
|
|
|
"statusCode": 200,
|
|
|
|
|
"headers": {"Content-Type": "text/plain"},
|
|
|
|
|
"body": payload.get("challenge", ""),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if payload.get("type") == "event_callback":
|
|
|
|
|
lambda_client.invoke(
|
|
|
|
|
FunctionName=PROCESSOR_FUNCTION_NAME,
|
|
|
|
|
InvocationType="Event",
|
|
|
|
|
Payload=json.dumps(payload["event"]).encode("utf-8"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {"statusCode": 200, "body": ""}
|
|
|
|
|
|
|
|
|
|
|
2026-04-20 18:01:48 -04:00
|
|
|
def get_signing_secret() -> str:
|
|
|
|
|
global _cached_signing_secret
|
|
|
|
|
if _cached_signing_secret:
|
|
|
|
|
return _cached_signing_secret
|
|
|
|
|
result = secrets_client.get_secret_value(SecretId=SLACK_SIGNING_SECRET_ARN)
|
|
|
|
|
_cached_signing_secret = result["SecretString"]
|
|
|
|
|
return _cached_signing_secret
|
|
|
|
|
|
|
|
|
|
|
2026-04-20 17:56:00 -04:00
|
|
|
def verify_signature(body: str, timestamp: str, signature: str) -> bool:
|
|
|
|
|
if not timestamp or not signature:
|
|
|
|
|
return False
|
|
|
|
|
try:
|
|
|
|
|
ts = int(timestamp)
|
|
|
|
|
except ValueError:
|
|
|
|
|
return False
|
|
|
|
|
if abs(time.time() - ts) > 300:
|
|
|
|
|
return False
|
2026-04-20 18:01:48 -04:00
|
|
|
secret = get_signing_secret()
|
2026-04-20 17:56:00 -04:00
|
|
|
base = f"v0:{timestamp}:{body}".encode("utf-8")
|
2026-04-20 18:01:48 -04:00
|
|
|
expected = "v0=" + hmac.new(secret.encode("utf-8"), base, hashlib.sha256).hexdigest()
|
2026-04-20 17:56:00 -04:00
|
|
|
return hmac.compare_digest(expected, signature)
|