Skip to content

HTTP Targets & Authentication

Tại Sao Quan Trọng

Cloud Tasks dispatch tasks tới HTTP endpoints bất kỳ, nhưng:

  • Endpoint phải public HTTP (có external IP)
  • Auth cần service account + OIDC tokens
  • Headers không thể trust (không phải auth mechanism)
  • Handler phải idempotent (at-least-once)

HTTP Target Endpoints

Supported Target Types

Cloud Tasks có thể dispatch tới:

✓ Cloud Run (HTTPS endpoint)
✓ App Engine (HTTPS endpoint)
✓ Compute Engine (public HTTP/HTTPS IP)
✓ GKE (public load balancer, Cloud Load Balancer frontend)
✓ On-premises (public internet)
✓ Any HTTP endpoint with external IP

Not Supported

✗ Cloud Functions (use Cloud Tasks buffering)
✗ Private internal IPs (no public endpoint)
✗ Custom handlers inside VPC (without public LB)

Task Creation

go
// Create task targeting Cloud Run service
client.CreateTask(ctx, &TaskRequest{
    Parent: "projects/X/locations/us-central1/queues/my-queue",
    Task: &Task{
        HttpRequest: &HttpRequest{
            Url: "https://my-service-XXXX-uc.a.run.app/api/webhook",
            HttpMethod: "POST",
            Headers: map[string]string{
                "Content-Type": "application/json",
            },
            Body: []byte(`{"id": "12345"}`),
        },
    },
})

Authentication: OIDC Tokens

Without Auth

go
// Public endpoint (no auth required)
client.CreateTask(ctx, &TaskRequest{
    Parent: "...queues/public",
    Task: &Task{
        HttpRequest: &HttpRequest{
            Url: "https://public-api.example.com/webhook",
            HttpMethod: "POST",
        },
    },
})

// Cloud Tasks sends: POST https://public-api.example.com/webhook
// No auth headers, endpoint public

With Auth

go
// Private endpoint (requires authentication)
client.CreateTask(ctx, &TaskRequest{
    Parent: "...queues/private",
    Task: &Task{
        HttpRequest: &HttpRequest{
            Url: "https://api.example.com/webhook",
            HttpMethod: "POST",
            OIDCToken: &OIDCToken{
                ServiceAccountEmail: "task-executor@my-project.iam.gserviceaccount.com",
                Audience: "https://api.example.com",  // Receiver's HTTPS origin
            },
        },
    },
})

// Cloud Tasks will:
// 1. Get ID token for service account
// 2. Sign token with private key
// 3. Include: Authorization: Bearer <id_token>
// 4. Send HTTP request

OIDC Token Mechanics

Flow

Step 1: Cloud Tasks request ID token
  POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/task-executor@X.iam.gserviceaccount.com:generateIdToken
  Audience: "https://api.example.com"

Step 2: Google STS returns signed JWT
  {
    "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
  }

Step 3: Cloud Tasks includes in request
  GET /webhook HTTP/1.1
  Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

Step 4: Handler validates token
  - Verify signature (public key from Google)
  - Check audience matches "https://api.example.com"
  - Check expiry (typically 1 hour)
  - Extract claims (service account email, etc.)

Token Validation (Go Example)

go
import (
    "context"
    "google.golang.org/api/idtoken"
)

func ValidateCloudTasksToken(ctx context.Context, bearerToken string, audience string) error {
    // Remove "Bearer " prefix
    token := strings.TrimPrefix(bearerToken, "Bearer ")
    
    // Verify and validate
    payload, err := idtoken.Validate(ctx, token, audience)
    if err != nil {
        return fmt.Errorf("invalid token: %w", err)
    }
    
    // Extract service account
    serviceAccount := payload.Claims["email"].(string)
    
    // Check if authorized
    if serviceAccount != "task-executor@my-project.iam.gserviceaccount.com" {
        return fmt.Errorf("unauthorized service account: %s", serviceAccount)
    }
    
    return nil
}

func WebhookHandler(w http.ResponseWriter, r *http.Request) {
    authHeader := r.Header.Get("Authorization")
    
    if authHeader == "" {
        // Public endpoint, no auth required
    } else {
        // Private endpoint, validate token
        err := ValidateCloudTasksToken(r.Context(), authHeader, "https://api.example.com")
        if err != nil {
            w.WriteHeader(http.StatusUnauthorized)
            fmt.Fprintf(w, `{"error": "%v"}`, err)
            return
        }
    }
    
    // Process task
    w.WriteHeader(http.StatusOK)
}

Setup: Service Account & IAM

Step 1: Create Service Account

bash
gcloud iam service-accounts create cloud-tasks-executor \
    --project=my-project \
    --display-name="Cloud Tasks Executor"

Step 2: Grant Permissions to Cloud Tasks

bash
# Let Cloud Tasks use this service account's credentials
gcloud projects add-iam-policy-binding my-project \
    --member=serviceAccount:cloud-tasks@PROJECT.iam.gserviceaccount.com \
    --role=roles/iam.serviceAccountUser \
    --condition=resource.name==serviceAccounts/cloud-tasks-executor@my-project.iam.gserviceaccount.com

Step 3: Grant Handler Permissions

If handler is Cloud Run or App Engine:

bash
# Handler trusts the service account
gcloud run services add-iam-policy-binding my-service \
    --member=serviceAccount:cloud-tasks-executor@my-project.iam.gserviceaccount.com \
    --role=roles/run.invoker

Headers Injected by Cloud Tasks

Cloud Tasks automatically includes metadata headers (NOT for authentication):

http
POST /webhook HTTP/1.1
Host: api.example.com
X-CloudTasks-QueueName: projects/my-project/locations/us-central1/queues/my-queue
X-CloudTasks-TaskName: projects/my-project/locations/us-central1/queues/my-queue/tasks/task-1
X-CloudTasks-TaskRetryCount: 0
X-CloudTasks-TaskETA: 1719360000

What These Are

X-CloudTasks-QueueName: Queue identifier (for reference)
X-CloudTasks-TaskName: Task ID (for idempotency tracking)
X-CloudTasks-TaskRetryCount: Retry attempt number (0 = first attempt)
X-CloudTasks-TaskETA: Scheduled execution time (Unix timestamp)

Important: These Are NOT Auth

go
// WRONG: Using headers for auth
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
    taskName := r.Header.Get("X-CloudTasks-TaskName")
    
    // WRONG: Anyone can set this header
    if taskName == "" {
        w.WriteHeader(http.StatusUnauthorized)
        return
    }
    
    // Process...
}

// An attacker can:
curl -H "X-CloudTasks-TaskName: projects/X/queues/Y/tasks/Z" \
     https://api.example.com/webhook

// And pass your "auth check"

Use OIDC tokens for authentication, not headers.


Special Case: App Engine

App Engine Task Handlers

App Engine can receive tasks with special headers:

http
X-AppEngine-QueueName: my-queue
X-AppEngine-TaskName: my-task
X-AppEngine-TaskRetryCount: 0
X-AppEngine-TaskExecutionCount: 1

Handler must return 200-299 for success.

Setup

go
package main

import (
    "net/http"
    "appengine"
)

func init() {
    http.HandleFunc("/_ah/queue/deferred", handleTask)
}

func handleTask(w http.ResponseWriter, r *http.Request) {
    queueName := r.Header.Get("X-AppEngine-QueueName")
    taskName := r.Header.Get("X-AppEngine-TaskName")
    
    // Process task
    err := processTask(r.Body)
    if err != nil {
        // 5xx → retry
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    
    // 200 → success
    w.WriteHeader(http.StatusOK)
}

Timeout Behavior

Queue-Level Timeout

Default timeout per target:

  • App Engine Standard: 10 minutes
  • App Engine Flex: 60 minutes
  • Cloud Run: depends on Cloud Run timeout (default 60 sec, max 3600 sec)

If handler doesn't respond within timeout:

Dispatch sent: 10:00:00
Timeout: 10 minutes
No response by: 10:10:00
Cloud Tasks: assume failed → retry

Handler Responsibility

Handler must complete within timeout:

go
func SlowHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    
    // If task takes 15 minutes, will timeout (if default 10 min)
    result := longRunningOperation(ctx)
    
    // Response might not reach Cloud Tasks
    // Cloud Tasks will retry
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, result)
}

Solution: Use background jobs for long-running work.

go
func Handler(w http.ResponseWriter, r *http.Request) {
    // Quick response
    task := parseTask(r)
    
    // Enqueue to background job system (e.g., Dataflow, Beam)
    err := backgroundJobQueue.Enqueue(task)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        return
    }
    
    // Return quickly
    w.WriteHeader(http.StatusOK)
}

Error Handling

Handler Response Code

200-299: SUCCESS (task completed)
300-399: RETRY (suspicious, shouldn't happen)
400: RETRY (bad request, might be transient)
401-403: RETRY (auth issue, token might refresh)
404: RETRY (endpoint might come back online)
429: RETRY (rate limited, backoff)
5xx: RETRY (server error, temporary)

No response / timeout: RETRY

What Not To Do

go
// WRONG: Returning success even when failed
func Handler(w http.ResponseWriter, r *http.Request) {
    err := processPayment()
    if err != nil {
        // WRONG: Return 200 OK anyway
        w.WriteHeader(http.StatusOK)
        fmt.Fprintf(w, `{"error": "%v"}`, err)
        return
    }
}

// Cloud Tasks sees 200 → thinks success → doesn't retry
// Payment failed, task deleted, no retry

Pattern: Fail Fast

go
// CORRECT: Return error status on failure
func Handler(w http.ResponseWriter, r *http.Request) {
    err := processPayment()
    if err != nil {
        // Return error status
        if isTransient(err) {
            w.WriteHeader(http.StatusServiceUnavailable) // 503
        } else {
            w.WriteHeader(http.StatusBadRequest) // 400 (won't retry forever)
        }
        fmt.Fprintf(w, `{"error": "%v"}`, err)
        return
    }
    
    w.WriteHeader(http.StatusOK)
}

Testing

Local Testing

bash
# Simulate Cloud Tasks dispatch
curl -X POST https://localhost:8080/webhook \
  -H "X-CloudTasks-QueueName: my-queue" \
  -H "X-CloudTasks-TaskName: my-task" \
  -H "X-CloudTasks-TaskRetryCount: 0" \
  -H "Content-Type: application/json" \
  -d '{"id": "12345"}'

Integration Testing

go
func TestPaymentWebhook(t *testing.T) {
    req := httptest.NewRequest("POST", "/webhook", bytes.NewReader([]byte(`{"id": "12345"}`)))
    req.Header.Set("X-CloudTasks-QueueName", "payment")
    req.Header.Set("X-CloudTasks-TaskName", "task-1")
    req.Header.Set("X-CloudTasks-TaskRetryCount", "0")
    
    w := httptest.NewRecorder()
    PaymentHandler(w, req)
    
    if w.Code != http.StatusOK {
        t.Errorf("expected 200, got %d", w.Code)
    }
}

References