I built a FastAPI App that receives meeting/recordings events from Zoom API through Zoom Webhook. I use ngrok for the webhook URL. Everything works perfectly when running it locally.
However, I built an image for the app in Docker, and when I try to run it, I get “signature rejected”.
I verified that I”m using the same environment variables, the same environment (even libraries version since I”m using poetry so the creation of the environment is specific and identical).
I verified that the container timezone is same as my host system timezone. But I’m still getting the error.
Here is the code of the signature verification function:
def signature_verified(self, headers: dict, body: bytes):
# Extract the necessary components from headers and body
timestamp = headers['x-zm-request-timestamp']
if not timestamp:
return False # Timestamp is missing, cannot verify
# Construct the message string
decoded_body = body.decode('utf-8')
message = f"v0:{timestamp}:{decoded_body}"
print("Constructed Message:", message)
#get zoom webhook secret token
secret_token = os.getenv('ZOOM_WEBHOOK_SECRET_TOKEN')
if not secret_token:
print("Secret token is missing.")
return False
# Create the HMAC SHA-256 hash
hash_for_verify = hmac.new(secret_token.encode(), message.encode(), hashlib.sha256).hexdigest()
# Create the signature
generated_signature = f"v0={hash_for_verify}"
print("Computed HMAC:", hash_for_verify)
human_readable = datetime.fromtimestamp(int(timestamp))
print("Timestamp from Zoom:", headers['x-zm-request-timestamp'])
print("Human-readable timestamp:", human_readable)
print("Generated Signature:", generated_signature)
print("Expected Signature:", headers.get('x-zm-signature'))
return headers.get('x-zm-signature') == generated_signature
Here is where the main.py where it triggers that function:
@app.post("/webhook")
async def webhook(request: Request):
# Get the request body and headers
client_host = request.client.host
print(f"Request from IP: {client_host}")
body = await request.body()
headers = request.headers
try:
# Verify the request came from Zoom
if zoom_client.signature_verified(headers, body):
# Parse the request body as JSON
event_data = await request.json()
# Check if this is a URL validation request
if event_data["event"] == "endpoint.url_validation":
logging.info("WEBHOOK: URL validation request received")
return zoom_client.url_verification(event_data)
elif event_data["event"] == "session.ended":
# Handle session.ended event
topic = event_data["payload"]["object"]["topic"]
id = event_data["payload"]["object"]["id"]
logging.info(f"WEBHOOK: Session ended: {topic}({id}))")
return {"message": "Received session.ended event."}
else:
logging.error("WEBHOOK: Unauthorized request to Webhook: Signature verified rejected")
return {"message": "Unauthorized request to Zoom Webhook"}, 401
except Exception as e:
logging.error(f"WEBHOOK: Error processing webhook: {e}")
return {"message": "Error processing Zoom Webhook"}, 500
pyproject.toml file:
[tool.poetry]
name = "zoomerizer"
version = "0.1.0"
description = "An app that summarizes the recordings of zoom meetings. It communicates with Zoom API."
authors = ["<email>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.109.2"
redis = "^5.0.1"
types-redis = "^4.6.0.20240218"
uvicorn = "^0.27.1"
python-dotenv = "^1.0.1"
requests = "^2.31.0"
logging = "^0.4.9.6"
coloredlogs = "^15.0.1"
verboselogs = "^1.7"
SQLAlchemy = "^2.0.27"
langchain = "^0.1.8"
openai = "^1.12.0"
langchain-openai = "^0.0.8"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Dockerfile:
# For more information, please refer to https://aka.ms/vscode-docker-python
FROM python:3-slim
# Set the working directory in the container
WORKDIR /app
# Keeps Python from generating .pyc files in the container
ENV PYTHONDONTWRITEBYTECODE=1
# Turns off buffering for easier container logging
ENV PYTHONUNBUFFERED=1
# Install Poetry
ENV POETRY_VERSION=1.8.2
RUN pip install "poetry==$POETRY_VERSION"
# Copy only requirements to cache them in docker layer
COPY pyproject.toml poetry.lock* ./
# Project initialization:
# This does not actually populate your project with Python packages
# It only copies the project setup files
RUN poetry config virtualenvs.create false
&& poetry install --no-dev --no-interaction --no-ansi
# Exposing the port that Gunicorn will run on
EXPOSE 8000
# Copying everything from the current directory into the container
COPY . /app
# Creates a non-root user with an explicit UID and adds permission to access the /app folder
# For more info, please refer to https://aka.ms/vscode-docker-python-configure-containers
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser:appuser /app
USER appuser
# During debugging, this entry point will be overridden. For more information, please refer to https://aka.ms/vscode-docker-python-debug
CMD ["uvicorn", "zoomerizer.main:app", "--host", "0.0.0.0", "--port", "8000"]
ngrok url (that is put in Zoom API Webhook) is forwarding to http://localhost:8000
And here is the error I get when I run this command:
docker run -e TZ=Europe/Berlin -p 8000:8000 --env-file .env zoomerizer
Error:
INFO: 192.168.65.1:64127 - "POST /webhook HTTP/1.1" 200 OK
2024-05-30 14:38:40,320 - ERROR - WEBHOOK: Unauthorized request to Webhook: Signature verified rejected
The signature generated when tested with Zoom signature it returns false. while locally without Docker it returns true Can you please help me identify the reason?
Thanks.