Skip to content

feat: Add trusted proxy email authentication for Cloud Run + IAP agent access#19

Open
adhishthite wants to merge 8 commits intoEveryInc:mainfrom
adhishthite:main
Open

feat: Add trusted proxy email authentication for Cloud Run + IAP agent access#19
adhishthite wants to merge 8 commits intoEveryInc:mainfrom
adhishthite:main

Conversation

@adhishthite
Copy link

@adhishthite adhishthite commented Mar 12, 2026

Summary

Proof SDK did not have a deployment path for agent access when hosted behind Cloud Run + Identity-Aware Proxy (IAP). In that setup, callers must first satisfy IAP (e.g., via an acme-corp.com identity) before Proof's own share-token auth applies. This PR adds support for trusting a proxy-injected email header as hosted auth, enabling agents to create and manage documents without a separate OAuth session.

Problem

Agent → Cloud Run (IAP) → Proof SDK
         ↓
   IAP injects x-goog-authenticated-user-email
         ↓
   Proof SDK had no way to trust this header
         ↓
   Agent forced into full OAuth flow (impractical for headless agents)

Agents running behind IAP-protected Cloud Run services couldn't authenticate with Proof SDK. The only auth paths were OAuth (requires browser interaction) or API keys (no identity tracking). This blocked production deployments where IAP already validates the caller.

Solution

Architecture

sequenceDiagram
    participant Agent
    participant IAP as Cloud IAP
    participant Proof as Proof SDK

    Agent->>IAP: Request + Google identity
    IAP->>IAP: Validate identity (acme-corp.com domain)
    IAP->>Proof: Forward + inject x-goog-authenticated-user-email
    Proof->>Proof: PROOF_TRUST_PROXY_HEADERS=true?
    alt Proxy trust enabled
        Proof->>Proof: Extract email from trusted header
        Proof->>Proof: Validate against allowed domains/emails
        Proof->>Proof: Set ownerId = authenticated email
        Proof-->>Agent: 200 OK (document created)
    else Proxy trust disabled
        Proof-->>Agent: Fall through to OAuth/API key auth
    end
Loading

Auth Flow Detail

flowchart TD
    A[Incoming Request] --> B{PROOF_TRUST_PROXY_HEADERS?}
    B -->|false| C[Skip proxy auth]
    B -->|true| D[Read configured email headers]
    D --> E{Header present?}
    E -->|no| C
    E -->|yes| F[Normalize email: strip IAP prefix, lowercase]
    F --> G{Email in allowed domains/emails?}
    G -->|no| C[Fall through to OAuth]
    G -->|yes| H[Trusted identity established]
    H --> I{ownerId in request?}
    I -->|no| J[Set ownerId = authenticated email]
    I -->|yes| K{ownerId matches authenticated email?}
    K -->|yes| J
    K -->|no| L[403 FORBIDDEN_OWNER_ID_MISMATCH]

    style L fill:#ff6b6b,color:#fff
    style J fill:#51cf66,color:#fff
    style H fill:#339af0,color:#fff
Loading

Configuration

# Enable trusted proxy headers (opt-in, default: false)
PROOF_TRUST_PROXY_HEADERS=true

# Which headers to trust (default: x-goog-authenticated-user-email only)
PROOF_TRUSTED_IDENTITY_EMAIL_HEADERS=x-goog-authenticated-user-email

# Restrict to specific domains and/or email addresses
PROOF_TRUSTED_IDENTITY_EMAIL_DOMAINS=acme-corp.com
PROOF_TRUSTED_IDENTITY_EMAILS=agent@acme-corp.com

# Auth mode must be set to oauth for trusted proxy to activate
PROOF_SHARE_MARKDOWN_AUTH_MODE=oauth

Example Usage

Once the caller has been admitted by IAP:

# Create a document via trusted proxy auth
curl -X POST "https://your-proof.example.com/api/share/markdown" \
  -H "Content-Type: application/json" \
  -H "X-Goog-Authenticated-User-Email: accounts.google.com:agent@acme-corp.com" \
  -d '{"markdown": "# Hello from IAP"}'

# ownerId is automatically set to agent@acme-corp.com
# Mismatched ownerId values are rejected with 403

Security Measures

This PR went through two rounds of security review. Key security properties:

What's protected

Attack Vector Mitigation
Discovery endpoint leaking config /.well-known/agent.json no longer exposes trusted headers, allowed emails, or allowed domains. Only advertises that trusted_proxy_email auth is available.
Header spoofing via x-forwarded-email Default trusted headers narrowed to only x-goog-authenticated-user-email. Generic spoofable headers are not trusted unless explicitly configured.
ownerId impersonation Body and query ownerId validated independently against the authenticated email. Mismatches return 403 FORBIDDEN_OWNER_ID_MISMATCH. Empty/whitespace values are also rejected.
Dual-source smuggling Both body.ownerId and ?ownerId= query param are checked separately. Conflicting values where one matches and one doesn't are rejected.
Auth source tracking New principalProvider field tracks whether auth came from none, api_key, oauth, or trusted_proxy_email, enabling per-source policy in downstream logic.

What operators must ensure

Critical: This feature assumes the proxy (IAP/load balancer) is the sole ingress path and strips any client-supplied copies of trusted headers before injecting its own. If the Proof SDK service is directly reachable without going through the proxy, header spoofing is trivial. Restrict Cloud Run ingress to internal + load balancer only.

Changes

Core

  • server/hosted-auth.ts — New module for trusted proxy identity resolution. Parses IAP-style email headers, normalizes emails (strips accounts.google.com: prefix, lowercases), validates against allowed domains/emails.
  • server/routes.tsauthorizeDirectShareRequest now tracks principalProvider across all auth paths. handleShareMarkdown enforces ownerId = authenticated email for trusted proxy auth with independent body/query validation.
  • server/discovery-routes.ts/.well-known/agent.json advertises trusted_proxy_email in auth.methods without exposing internal config.

Docs

  • README.md — Cloud Run + IAP setup section with configuration example
  • docs/agent-docs.md — Agent-side flow documentation
  • .env.example — New env vars documented

Tests (8 new, 83 total)

  • Trusted proxy email header acceptance
  • Discovery endpoint config redaction
  • x-forwarded-email not trusted by default
  • ownerId mismatch rejection (body)
  • ownerId mismatch rejection (query param only)
  • Dual-source ownerId conflict (body matches, query doesn't)
  • Empty/whitespace ownerId rejection
  • API key auth regression (arbitrary ownerId still allowed)

Rate Limiting

  • Shared rate limiting added to document access/auth endpoints involved in these flows

Testing

npm run test
# 83/83 passing

Review History

  1. Initial review — Found 3 blocking issues (discovery leak, default headers too broad, ownerId override)
  2. Fix round 1 — All 3 blockers resolved, 3 new tests
  3. Security review round 2 — Found hardening gaps (dual-source ownerId, empty string bypass, test coverage)
  4. Fix round 2 — All hardening issues resolved, 5 more tests
  5. Final security reviewAPPROVED

Copilot AI and others added 8 commits March 12, 2026 16:34
Co-authored-by: adhishthite <31769894+adhishthite@users.noreply.github.com>
Co-authored-by: adhishthite <31769894+adhishthite@users.noreply.github.com>
Remove email_headers, allowed_email_domains, and allowed_emails from
/.well-known/agent.json public discovery. Only advertise that
trusted_proxy_email auth is available, not internal config details.

Add test: D2: discovery endpoint redacts trusted proxy allowlists
Remove x-forwarded-email from the default trusted headers fallback.
When PROOF_TRUSTED_IDENTITY_EMAIL_HEADERS is not set, only
x-goog-authenticated-user-email is trusted by default.

Add test: D2: trusted proxy auth does not trust x-forwarded-email
For trusted_proxy_email auth, force ownerId to the authenticated email.
If body.ownerId differs from the authenticated principal, reject with
403 FORBIDDEN_OWNER_ID_MISMATCH. Add principalProvider field to
DirectShareAuthorizationResult for auth source tracking.

Update docs to reflect that ownerId is now enforced, not optional.
Add test: D2: trusted proxy auth rejects ownerId mismatches
- Validate body.ownerId and query.ownerId independently (not coalesced)
- Reject empty/whitespace-only ownerId strings for trusted_proxy_email auth
- Add isRejectedTrustedProxyOwnerId helper for clean validation logic
- 5 new tests: query-only mismatch, dual-source conflict, empty ownerId,
  whitespace ownerId, api_key regression (arbitrary ownerId still allowed)
…gents

Add trusted proxy email auth for Cloud Run + IAP agent access
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants