Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ NEO4J_DATABASE=neo4j
GITHUB_APP_ID=your_app_id_here
GITHUB_PRIVATE_KEY_PATH=/path/to/your-app-private-key.pem
GITHUB_WEBHOOK_SECRET=your_webhook_secret_here
GITHUB_MARKETPLACE_WEBHOOK_SECRET=your_marketplace_webhook_secret_here

# ============================================================================
# Firebase (service account — JSON string or file path)
Expand Down
49 changes: 49 additions & 0 deletions src/api/middleware/webhook_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import hashlib
import hmac
import logging
import os

from fastapi import HTTPException, Request

logger = logging.getLogger(__name__)


def _verify_signature(raw_body: bytes, secret: str, signature_header: str) -> None:
if not signature_header or not signature_header.startswith("sha256="):
raise HTTPException(status_code=403, detail="Missing or malformed X-Hub-Signature-256 header")
local_hash = hmac.new(
key=secret.encode("utf-8"),
msg=raw_body,
digestmod=hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(local_hash, signature_header.removeprefix("sha256=")):
raise HTTPException(status_code=403, detail="Invalid webhook signature")


async def verify_github_webhook_signature(request: Request) -> None:
secret = os.environ.get("GITHUB_WEBHOOK_SECRET")
if not secret:
logger.error("GITHUB_WEBHOOK_SECRET is not set")
raise HTTPException(status_code=500, detail="Webhook secret not configured")
_verify_signature(
await request.body(),
secret,
request.headers.get("X-Hub-Signature-256", ""),
)


async def verify_marketplace_webhook_signature(request: Request) -> None:
secret = os.environ.get("GITHUB_MARKETPLACE_WEBHOOK_SECRET")
if not secret:
logger.warning(
"GITHUB_MARKETPLACE_WEBHOOK_SECRET is not set — falling back to GITHUB_WEBHOOK_SECRET"
)
secret = os.environ.get("GITHUB_WEBHOOK_SECRET")
if not secret:
logger.error("Neither GITHUB_MARKETPLACE_WEBHOOK_SECRET nor GITHUB_WEBHOOK_SECRET is set")
raise HTTPException(status_code=500, detail="Webhook secret not configured")
_verify_signature(
await request.body(),
secret,
request.headers.get("X-Hub-Signature-256", ""),
)
10 changes: 7 additions & 3 deletions src/api/routers/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
import logging
import uuid

from fastapi import APIRouter, BackgroundTasks, Request
from fastapi import APIRouter, BackgroundTasks, Depends, Request

from api.middleware.webhook_auth import (
verify_github_webhook_signature,
verify_marketplace_webhook_signature,
)
from api.services.cloud_tasks_service import CloudTasksService
from api.services.code_review_commands import extract_review_command, is_bot_mentioned
from common.firebase_service import firebase_service
Expand All @@ -22,7 +26,7 @@
cloud_tasks = CloudTasksService()


@router.post("/onComment")
@router.post("/onComment", dependencies=[Depends(verify_github_webhook_signature)])
async def on_comment(request: Request, background_tasks: BackgroundTasks):
"""
Single GitHub webhook endpoint. Routes all events by X-GitHub-Event header:
Expand Down Expand Up @@ -289,7 +293,7 @@ async def _handle_comment_review(payload: dict, background_tasks: BackgroundTask
}


@router.post("/marketplace")
@router.post("/marketplace", dependencies=[Depends(verify_marketplace_webhook_signature)])
async def on_marketplace(request: Request):
"""
GitHub Marketplace webhook.
Expand Down