[Jira]: separate signature verification from conversation orchestration (#12478)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Rohit Malhotra
2026-01-20 13:21:23 -08:00
committed by GitHub
parent 87a5762bf2
commit 3bc56740b9
5 changed files with 394 additions and 176 deletions

View File

@@ -1,3 +1,5 @@
import hashlib
import hmac
import json
import os
import re
@@ -5,7 +7,7 @@ import uuid
from urllib.parse import urlparse
import requests
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request, status
from fastapi import APIRouter, BackgroundTasks, Header, HTTPException, Request, status
from fastapi.responses import JSONResponse, RedirectResponse
from integrations.jira.jira_manager import JiraManager
from integrations.models import Message, SourceType
@@ -14,6 +16,7 @@ from pydantic import BaseModel, Field, field_validator
from server.auth.constants import JIRA_CLIENT_ID, JIRA_CLIENT_SECRET
from server.auth.saas_user_auth import SaasUserAuth
from server.auth.token_manager import TokenManager
from storage.jira_workspace import JiraWorkspace
from storage.redis import create_redis_client
from openhands.core.logger import openhands_logger as logger
@@ -122,6 +125,63 @@ jira_manager = JiraManager(token_manager)
redis_client = create_redis_client()
async def verify_jira_signature(body: bytes, signature: str, payload: dict):
"""
Verify Jira webhook signature.
Args:
body: Raw request body bytes
signature: Signature from x-hub-signature header (format: "sha256=<hash>")
payload: Parsed JSON payload from webhook
Raises:
HTTPException: 403 if signature verification fails or workspace is invalid
Returns:
None (raises exception on failure)
"""
if not signature:
raise HTTPException(
status_code=403, detail='x-hub-signature header is missing!'
)
workspace_name = jira_manager.get_workspace_name_from_payload(payload)
if workspace_name is None:
logger.warning('[Jira] No workspace name found in webhook payload')
raise HTTPException(
status_code=403, detail='Workspace name not found in payload'
)
workspace: (
JiraWorkspace | None
) = await jira_manager.integration_store.get_workspace_by_name(workspace_name)
if workspace is None:
logger.warning(f'[Jira] Could not identify workspace {workspace_name}')
raise HTTPException(status_code=403, detail='Unidentified workspace')
if workspace.status != 'active':
logger.warning(
'[Jira] Workspace is inactive',
extra={
'jira_workspace_id': workspace.id,
'parsed_workspace_name': workspace.name,
'status': workspace.status,
},
)
raise HTTPException(status_code=403, detail='Workspace is inactive')
webhook_secret = token_manager.decrypt_text(workspace.webhook_secret)
expected_signature = hmac.new(
webhook_secret.encode(), body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected_signature, signature):
raise HTTPException(status_code=403, detail="Request signatures didn't match!")
async def _handle_workspace_link_creation(
user_id: str, jira_user_id: str, target_workspace: str
):
@@ -216,6 +276,7 @@ async def _validate_workspace_update_permissions(user_id: str, target_workspace:
async def jira_events(
request: Request,
background_tasks: BackgroundTasks,
x_hub_signature: str = Header(None),
):
"""Handle Jira webhook events."""
# Check if Jira webhooks are enabled
@@ -227,13 +288,15 @@ async def jira_events(
)
try:
signature_valid, signature, payload = await jira_manager.validate_request(
request
)
parts = x_hub_signature.split('=', 1)
if not (len(parts) == 2 and parts[1]):
raise HTTPException(status_code=403, detail='Malformed x-hub-signature!')
if not signature_valid:
logger.warning('[Jira] Invalid webhook signature')
raise HTTPException(status_code=403, detail='Invalid webhook signature!')
signature = parts[1]
body = await request.body()
payload = await request.json()
await verify_jira_signature(body, signature, payload)
# Check for duplicate requests using Redis
key = f'jira:{signature}'