From b00749a8a63cd0369a5f1f426a273abe9dff5ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stellar=E9=B1=BC?= <2182712990@qq.com> Date: Tue, 26 May 2026 23:19:57 +0800 Subject: [PATCH] fix(auth): share internal gateway token across workers (#3184) * fix(auth): share internal gateway token across workers * fix: restore deploy script executable bit * Update deploy.sh to skip the auth_token setup for the down command --------- Co-authored-by: Willem Jiang --- .env.example | 5 ++++ backend/app/gateway/internal_auth.py | 19 +++++++++++---- backend/tests/test_internal_auth.py | 35 ++++++++++++++++++++++++++++ docker/docker-compose-dev.yaml | 1 + docker/docker-compose.yaml | 2 ++ scripts/deploy.sh | 35 +++++++++++++++++++++++++++- 6 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 backend/tests/test_internal_auth.py diff --git a/.env.example b/.env.example index 43290954b..c4dbe326e 100644 --- a/.env.example +++ b/.env.example @@ -50,6 +50,11 @@ INFOQUEST_API_KEY=your-infoquest-api-key # Set to "false" to disable Swagger UI, ReDoc, and OpenAPI schema in production # GATEWAY_ENABLE_DOCS=false +# Shared internal Gateway auth token for multi-worker deployments. +# `make up` generates and persists this automatically; set it manually only +# when you run Gateway workers outside the bundled deploy script. +# DEER_FLOW_INTERNAL_AUTH_TOKEN=your-shared-internal-token + # ── Frontend SSR → Gateway wiring ───────────────────────────────────────────── # The Next.js server uses these to reach the Gateway during SSR (auth checks, # /api/* rewrites). They default to localhost values that match `make dev` and diff --git a/backend/app/gateway/internal_auth.py b/backend/app/gateway/internal_auth.py index b0380379b..51ed89a99 100644 --- a/backend/app/gateway/internal_auth.py +++ b/backend/app/gateway/internal_auth.py @@ -1,23 +1,34 @@ -"""Process-local authentication for Gateway internal callers.""" +"""Authentication for trusted Gateway internal callers.""" from __future__ import annotations +import os import secrets from types import SimpleNamespace from deerflow.runtime.user_context import DEFAULT_USER_ID INTERNAL_AUTH_HEADER_NAME = "X-DeerFlow-Internal-Token" -_INTERNAL_AUTH_TOKEN = secrets.token_urlsafe(32) +INTERNAL_AUTH_ENV_VAR = "DEER_FLOW_INTERNAL_AUTH_TOKEN" + + +def _load_internal_auth_token() -> str: + token = os.environ.get(INTERNAL_AUTH_ENV_VAR) + if token: + return token + return secrets.token_urlsafe(32) + + +_INTERNAL_AUTH_TOKEN = _load_internal_auth_token() def create_internal_auth_headers() -> dict[str, str]: - """Return headers that authenticate same-process Gateway internal calls.""" + """Return headers that authenticate trusted Gateway internal calls.""" return {INTERNAL_AUTH_HEADER_NAME: _INTERNAL_AUTH_TOKEN} def is_valid_internal_auth_token(token: str | None) -> bool: - """Return True when *token* matches the process-local internal token.""" + """Return True when *token* matches this Gateway worker's internal token.""" return bool(token) and secrets.compare_digest(token, _INTERNAL_AUTH_TOKEN) diff --git a/backend/tests/test_internal_auth.py b/backend/tests/test_internal_auth.py new file mode 100644 index 000000000..7e56e1dd0 --- /dev/null +++ b/backend/tests/test_internal_auth.py @@ -0,0 +1,35 @@ +"""Tests for Gateway internal auth token handling.""" + +from __future__ import annotations + +import importlib + + +def test_internal_auth_uses_shared_env_token(monkeypatch): + import app.gateway.internal_auth as internal_auth + + monkeypatch.setenv("DEER_FLOW_INTERNAL_AUTH_TOKEN", "shared-token") + reloaded = importlib.reload(internal_auth) + try: + headers = reloaded.create_internal_auth_headers() + + assert headers[reloaded.INTERNAL_AUTH_HEADER_NAME] == "shared-token" + assert reloaded.is_valid_internal_auth_token("shared-token") is True + assert reloaded.is_valid_internal_auth_token("other-token") is False + finally: + monkeypatch.delenv("DEER_FLOW_INTERNAL_AUTH_TOKEN", raising=False) + importlib.reload(reloaded) + + +def test_internal_auth_generates_process_local_fallback(monkeypatch): + import app.gateway.internal_auth as internal_auth + + monkeypatch.delenv("DEER_FLOW_INTERNAL_AUTH_TOKEN", raising=False) + reloaded = importlib.reload(internal_auth) + try: + token = reloaded.create_internal_auth_headers()[reloaded.INTERNAL_AUTH_HEADER_NAME] + + assert token + assert reloaded.is_valid_internal_auth_token(token) is True + finally: + importlib.reload(reloaded) diff --git a/docker/docker-compose-dev.yaml b/docker/docker-compose-dev.yaml index b2e15680f..233d22c55 100644 --- a/docker/docker-compose-dev.yaml +++ b/docker/docker-compose-dev.yaml @@ -168,6 +168,7 @@ services: - DEER_FLOW_HOME=/app/backend/.deer-flow - DEER_FLOW_CHANNELS_LANGGRAPH_URL=${DEER_FLOW_CHANNELS_LANGGRAPH_URL:-http://gateway:8001/api} - DEER_FLOW_CHANNELS_GATEWAY_URL=${DEER_FLOW_CHANNELS_GATEWAY_URL:-http://gateway:8001} + - DEER_FLOW_INTERNAL_AUTH_TOKEN=${DEER_FLOW_INTERNAL_AUTH_TOKEN:-} - DEER_FLOW_HOST_BASE_DIR=${DEER_FLOW_ROOT}/backend/.deer-flow - DEER_FLOW_HOST_SKILLS_PATH=${DEER_FLOW_ROOT}/skills - DEER_FLOW_SANDBOX_HOST=host.docker.internal diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 8d82980d3..169e8f3d9 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -16,6 +16,7 @@ # DEER_FLOW_DOCKER_SOCKET — Docker socket path, default /var/run/docker.sock # DEER_FLOW_REPO_ROOT — repo root (used for skills host path in DooD) # BETTER_AUTH_SECRET — required for frontend auth/session security +# DEER_FLOW_INTERNAL_AUTH_TOKEN — shared internal Gateway auth token for multi-worker IM channels # # LangSmith tracing is disabled by default (LANGSMITH_TRACING=false). # Set LANGSMITH_TRACING=true and LANGSMITH_API_KEY in .env to enable it. @@ -101,6 +102,7 @@ services: - DEER_FLOW_EXTENSIONS_CONFIG_PATH=/app/backend/extensions_config.json - DEER_FLOW_CHANNELS_LANGGRAPH_URL=${DEER_FLOW_CHANNELS_LANGGRAPH_URL:-http://gateway:8001/api} - DEER_FLOW_CHANNELS_GATEWAY_URL=${DEER_FLOW_CHANNELS_GATEWAY_URL:-http://gateway:8001} + - DEER_FLOW_INTERNAL_AUTH_TOKEN=${DEER_FLOW_INTERNAL_AUTH_TOKEN} # DooD path/network translation - DEER_FLOW_HOST_BASE_DIR=${DEER_FLOW_HOME} - DEER_FLOW_HOST_SKILLS_PATH=${DEER_FLOW_REPO_ROOT}/skills diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 41c9dfa3f..bfef7e30e 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -71,7 +71,7 @@ if [ -z "$DEER_FLOW_CONFIG_PATH" ]; then export DEER_FLOW_CONFIG_PATH="$REPO_ROOT/config.yaml" fi -if [ ! -f "$DEER_FLOW_CONFIG_PATH" ]; then +if [ "$CMD" != "down" ] && [ ! -f "$DEER_FLOW_CONFIG_PATH" ]; then # Try to seed from repo (config.example.yaml is the canonical template) if [ -f "$REPO_ROOT/config.example.yaml" ]; then cp "$REPO_ROOT/config.example.yaml" "$DEER_FLOW_CONFIG_PATH" @@ -140,6 +140,38 @@ if [ -z "$BETTER_AUTH_SECRET" ]; then fi fi +# ── DEER_FLOW_INTERNAL_AUTH_TOKEN ──────────────────────────────────────────── +# Shared by all Gateway workers so channel workers can call internal Gateway +# APIs even when the request is handled by a different Uvicorn worker. + +_internal_auth_token_file="$DEER_FLOW_HOME/.internal-auth-token" +if [ "$CMD" != "down" ] && [ -z "$DEER_FLOW_INTERNAL_AUTH_TOKEN" ]; then + if [ -f "$_internal_auth_token_file" ]; then + export DEER_FLOW_INTERNAL_AUTH_TOKEN + DEER_FLOW_INTERNAL_AUTH_TOKEN="$(cat "$_internal_auth_token_file")" + echo -e "${GREEN}✓ DEER_FLOW_INTERNAL_AUTH_TOKEN loaded from $_internal_auth_token_file${NC}" + else + export DEER_FLOW_INTERNAL_AUTH_TOKEN + if command -v python3 > /dev/null 2>&1 && \ + DEER_FLOW_INTERNAL_AUTH_TOKEN="$(python3 -c 'import sys; sys.version_info >= (3, 6) or sys.exit(1); import secrets; print(secrets.token_urlsafe(32))' 2>/dev/null)"; then + true + elif command -v python > /dev/null 2>&1 && \ + DEER_FLOW_INTERNAL_AUTH_TOKEN="$(python -c 'import sys; sys.version_info >= (3, 6) or sys.exit(1); import secrets; print(secrets.token_urlsafe(32))' 2>/dev/null)"; then + true + elif command -v openssl > /dev/null 2>&1 && \ + DEER_FLOW_INTERNAL_AUTH_TOKEN="$(openssl rand -hex 32)"; then + true + else + echo -e "${RED}✗ Cannot generate DEER_FLOW_INTERNAL_AUTH_TOKEN: python3, python, and openssl are all unavailable.${NC}" >&2 + echo -e "${RED} Set DEER_FLOW_INTERNAL_AUTH_TOKEN manually before running make up.${NC}" >&2 + exit 1 + fi + echo "$DEER_FLOW_INTERNAL_AUTH_TOKEN" > "$_internal_auth_token_file" + chmod 600 "$_internal_auth_token_file" + echo -e "${GREEN}✓ DEER_FLOW_INTERNAL_AUTH_TOKEN generated → $_internal_auth_token_file${NC}" + fi +fi + # ── detect_sandbox_mode ─────────────────────────────────────────────────────── detect_sandbox_mode() { @@ -186,6 +218,7 @@ if [ "$CMD" = "down" ]; then export DEER_FLOW_DOCKER_SOCKET="${DEER_FLOW_DOCKER_SOCKET:-/var/run/docker.sock}" export DEER_FLOW_REPO_ROOT="${DEER_FLOW_REPO_ROOT:-$REPO_ROOT}" export BETTER_AUTH_SECRET="${BETTER_AUTH_SECRET:-placeholder}" + export DEER_FLOW_INTERNAL_AUTH_TOKEN="${DEER_FLOW_INTERNAL_AUTH_TOKEN:-placeholder}" "${COMPOSE_CMD[@]}" down exit 0 fi