From 7d2366d0f49caf3fa2156729a6d52dbbcd53eb4d Mon Sep 17 00:00:00 2001 From: michael Date: Fri, 2 Jan 2026 11:41:07 -0600 Subject: [PATCH] fix: add authentication for Cloud Run service calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cloud Run services are deployed with --no-allow-unauthenticated, requiring an ID token in the Authorization header. - Add _get_cloud_run_id_token() helper using google-auth library - Update whisper_transcribe.py to include Bearer token in Cloud Run calls - Update video_renderer.py to include Bearer token in FFmpeg Cloud Run calls The ID token is fetched using the service account credentials (GOOGLE_APPLICATION_CREDENTIALS) and targets the Cloud Run service URL. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- backend/app/services/video_renderer.py | 32 +++++++++++++++++++++---- backend/app/tasks/whisper_transcribe.py | 30 ++++++++++++++++++++++- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/backend/app/services/video_renderer.py b/backend/app/services/video_renderer.py index 1fbcb3c..48fe9c3 100644 --- a/backend/app/services/video_renderer.py +++ b/backend/app/services/video_renderer.py @@ -14,8 +14,11 @@ from pathlib import Path from typing import Any from uuid import uuid4 +import google.auth.transport.requests import httpx +from google.auth import default from google.cloud import storage +from google.oauth2 import id_token from ..core.config import settings from ..core.logging import get_logger @@ -24,6 +27,19 @@ from ..schemas.accessible_video import AccessibleVideoMethod, GeminiAccessibleVi logger = get_logger(__name__) +def _get_cloud_run_id_token(audience: str) -> str: + """ + Get an ID token for authenticating to Cloud Run services. + + Uses the service account credentials to generate an ID token + that Cloud Run will accept for authentication. + """ + credentials, _ = default() + request = google.auth.transport.requests.Request() + token = id_token.fetch_id_token(request, audience) + return token + + class FFmpegExecutionError(Exception): """Raised when an FFmpeg/FFprobe command fails.""" pass @@ -153,9 +169,13 @@ class VideoRendererService: Probe result with duration and stream info """ client = self._get_http_client() + service_url = settings.ffmpeg_service_url.rstrip("/") + auth_token = _get_cloud_run_id_token(service_url) + response = client.post( - f"{settings.ffmpeg_service_url}/probe", - json={"source_gcs_uri": gcs_uri} + f"{service_url}/probe", + json={"source_gcs_uri": gcs_uri}, + headers={"Authorization": f"Bearer {auth_token}"} ) response.raise_for_status() return response.json() @@ -178,10 +198,14 @@ class VideoRendererService: Response JSON """ client = self._get_http_client() + service_url = settings.ffmpeg_service_url.rstrip("/") + auth_token = _get_cloud_run_id_token(service_url) + try: response = client.post( - f"{settings.ffmpeg_service_url}{endpoint}", - json=payload + f"{service_url}{endpoint}", + json=payload, + headers={"Authorization": f"Bearer {auth_token}"} ) response.raise_for_status() return response.json() diff --git a/backend/app/tasks/whisper_transcribe.py b/backend/app/tasks/whisper_transcribe.py index 3e2deda..18c893c 100644 --- a/backend/app/tasks/whisper_transcribe.py +++ b/backend/app/tasks/whisper_transcribe.py @@ -3,8 +3,11 @@ import os import uuid +import google.auth.transport.requests import httpx +from google.auth import default from google.cloud import storage +from google.oauth2 import id_token from ..core.config import settings from ..core.logging import get_logger @@ -14,6 +17,25 @@ from . import celery_app logger = get_logger(__name__) +def _get_cloud_run_id_token(audience: str) -> str: + """ + Get an ID token for authenticating to Cloud Run services. + + Uses the service account credentials to generate an ID token + that Cloud Run will accept for authentication. + """ + # Get credentials from the environment (GOOGLE_APPLICATION_CREDENTIALS) + credentials, _ = default() + + # Create a request object for token refresh + request = google.auth.transport.requests.Request() + + # Fetch an ID token for the target audience (the Cloud Run service URL) + token = id_token.fetch_id_token(request, audience) + + return token + + def _upload_audio_to_gcs_temp(audio_path: str, job_id: str) -> str: """Upload local audio file to GCS temporary location and return GCS URI.""" # Generate unique temp path @@ -70,11 +92,17 @@ def _transcribe_via_cloud_run(job_id: str, audio_path: str) -> dict: logger.info(f"Calling Whisper Cloud Run service: {endpoint}") + # Get ID token for Cloud Run authentication + id_token = _get_cloud_run_id_token(service_url) + with httpx.Client(timeout=300.0) as client: response = client.post( endpoint, json={"gcs_uri": gcs_uri}, - headers={"Content-Type": "application/json"} + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {id_token}" + } ) response.raise_for_status()