RSS Amplifier

The Agent Native Product · Jan 23, 2026

Adding Authentication to LangGraph on Heroku (Without a License)

0
Sign in to vote or save

Alex Key · The Agent Native Product

This is a follow-up to Deploying a LangGraph Agent to Heroku. Once your agent is deployed, you’ll want to protect it from unauthorized access. This guide shows how to add API key authentication without needing a LangGraph Cloud license.

LangGraph Cloud offers built-in authentication, but it requires a paid license. For small deployments or prototypes, you might want a simpler solution that:

  • Validates API keys before forwarding requests to the agent

  • Works with the existing LangGraph SDK

  • Supports streaming responses (Server-Sent Events)

  • Keeps the API key on your backend, never exposing it to browsers

We’ll create a lightweight Python reverse proxy using Starlette that:

  1. Sits in front of the LangGraph server

  2. Validates incoming API keys

  3. Forwards authenticated requests to LangGraph

  4. Properly handles streaming responses

The architecture looks like this:

Browser → Next.js Backend → Auth Proxy → LangGraph Server
                              ↑
                        Validates API key

The proxy runs on the same Heroku dyno as LangGraph, so there’s no additional infrastructure. All communication between the proxy and LangGraph happens over (the Docker container’s) localhost.

Create agent/auth_proxy.py:

"""
Simple authentication proxy for LangGraph API.
Validates API key from Authorization header before forwarding requests.
"""
import os
import httpx
from starlette.applications import Starlette
from starlette.requests import Request
from starlette.responses import Response, StreamingResponse
from starlette.routing import Route
API_KEY = os.environ.get("LANGGRAPH_API_KEY", "")
LANGGRAPH_URL = os.environ.get("LANGGRAPH_URL", "http://localhost:8000")
# Persistent client for connection pooling
client = httpx.AsyncClient(timeout=httpx.Timeout(300.0, connect=10.0))
async def proxy(request: Request) -> Response:
    if not API_KEY:
        return Response("API_KEY not configured", status_code=500)
    # Accept both X-Api-Key header (LangGraph SDK) and Authorization: Bearer
    x_api_key = request.headers.get("X-Api-Key", "")
    auth_header = request.headers.get("Authorization", "")
    expected_bearer = f"Bearer {API_KEY}"
    if x_api_key != API_KEY and auth_header != expected_bearer:
        return Response("Unauthorized", status_code=401)
    path = request.url.path
    query = request.url.query
    url = f"{LANGGRAPH_URL}{path}"
    if query:
        url = f"{url}?{query}"
    body = await request.body()
    headers = dict(request.headers)
    headers.pop("host", None)
    headers.pop("authorization", None)
    headers.pop("content-length", None)
    # Check if this is a streaming endpoint
    is_stream = "/stream" in path
    if is_stream:
        req = client.build_request(
            method=request.method,
            url=url,
            headers=headers,
            content=body,
        )
        response = await client.send(req, stream=True)
        async def stream_response():
            try:
                async for chunk in response.aiter_bytes():
                    yield chunk
            finally:
                await response.aclose()
        return StreamingResponse(
            stream_response(),
            status_code=response.status_code,
            headers=dict(response.headers),
        )
    else:
        response = await client.request(
            method=request.method,
            url=url,
            headers=headers,
            content=body,
        )
        return Response(
            content=response.content,
            status_code=response.status_code,
            headers=dict(response.headers),
        )
app = Starlette(
    routes=[
        Route("/{path:path}", proxy, methods=["GET", "POST", "PUT", "DELETE", "PATCH"]),
        Route("/", proxy, methods=["GET", "POST", "PUT", "DELETE", "PATCH"]),
    ]
)

Key features of this proxy:

  • Dual header support: Accepts both X-Api-Key (what LangGraph SDK sends) and Authorization: Bearer (standard pattern). This ensures compatibility with different clients.

  • Streaming support: Endpoints containing /stream in the path are handled with StreamingResponse for Server-Sent Events. This is critical for the chat interface - without it, you’d only see responses after the full generation completes.

  • Connection pooling: The httpx.AsyncClient is created at module level, not per-request. This reuses TCP connections and significantly reduces latency.

  • Header stripping: We remove host, authorization, and content-length before forwarding. The host header would be wrong (it’s the proxy’s host, not LangGraph’s), authorization is our key (not needed internally), and content-length gets recalculated by httpx.

The LANGGRAPH_API_KEY is our own creation – it’s the shared secret between our frontend and the agent.

Replace the simple entrypoint from the deployment guide with this version that starts both the LangGraph server and the auth proxy. The original entrypoint just mapped environment variables and called LangGraph directly. This version runs LangGraph in the background and puts the auth proxy in front of it.

Modify agent/heroku-entrypoint.sh:

#!/bin/sh
# Map Heroku env var names to LangGraph expected names
export DATABASE_URI="$DATABASE_URL"
# Only add ssl_cert_reqs for SSL Redis connections (Heroku uses rediss://)
case "$REDIS_URL" in
    rediss://*)
        export REDIS_URI="${REDIS_URL}?ssl_cert_reqs=none"
        ;;
    *)
        export REDIS_URI="$REDIS_URL"
        ;;
esac
# Save the original PORT for the auth proxy
AUTH_PROXY_PORT="$PORT"
# Start LangGraph on internal port 8000
export PORT=8000
/storage/entrypoint.sh &
LANGGRAPH_PID=$!
# Wait for LangGraph to be ready
echo "Waiting for LangGraph to start..."
sleep 5
# Start auth proxy on the original Heroku port
echo "Starting auth proxy on port $AUTH_PROXY_PORT..."
exec uvicorn auth_proxy:app --host 0.0.0.0 --port "$AUTH_PROXY_PORT"

This script:

(See part one of this tutorial for details on points 1 and 2):

  1. Maps Heroku’s environment variables to LangGraph’s expected names

  2. Conditionally adds SSL cert skipping only for rediss:// URLs (production), not redis://(local)

  3. Starts LangGraph on internal port 8000 in the background

  4. Waits 5 seconds for LangGraph to initialize

  5. Starts the auth proxy on Heroku’s assigned $PORT using exec (so signals are properly forwarded)

The 5-second sleep is a simple approach. For more robust deployments, you could implement a health check loop that polls http://localhost:8000/info until it responds.

Update agent/pyproject.toml to include the required packages:

[project]
name = "your-agent"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
    "httpx>=0.27.0",
    "langchain>=1.2.0",
    "langchain-openai>=0.3.35",
    "starlette>=0.38.0",
    "uvicorn>=0.30.0",
    # ... your other dependencies
]
[project.optional-dependencies]
dev = [
    "langgraph-cli[inmem]>=0.4.3",
]
[tool.setuptools]
py-modules = ["agent", "auth_proxy"]

The key additions are:

  • httpx: Async HTTP client for proxying requests with connection pooling

  • starlette: Lightweight ASGI framework for the proxy

  • uvicorn: ASGI server to run the proxy

  • auth_proxy in py-modules: Makes the proxy module importable by uvicorn

Note: langgraph-cli is only needed for local development as it comes with the base image from LangGraph anyway.

Heroku config vars:

heroku config:set LANGGRAPH_API_KEY=your-secret-api-key -a your-app-name

The LANGGRAPH_API_KEY is what any client will need to send along as an api-key.

You may generate a strong hash like so:

openssl rand -hex 32

Test that authentication is working:

# Should return 401 Unauthorized
curl https://your-app-name.herokuapp.com/
# Should return 200 OK
curl -H "x-api-key: your-secret-api-key" https://your-app-name.herokuapp.com/ok

After implementing authentication, your project structure should look like this:

your-project/
├── heroku.yml
├── frontend/
│   ├── (optional - if you have a monorepo)
└── agent/
    ├── agent.py
    ├── auth_proxy.py          # New: authentication proxy
    ├── langgraph.json
    ├── pyproject.toml         # Updated: added httpx, starlette, uvicorn
    ├── Dockerfile
    └── heroku-entrypoint.sh   # Updated: starts both processes

The auth proxy can’t reach the LangGraph server. Check:

  • LangGraph is running on port 8000: run heroku logs --tail -a your-app and look for “Starting Postgres runtime”

  • The 5-second sleep might not be enough - increase if needed

  • Check that LANGGRAPH_URL defaults to

http://localhost:8000

  • (not https)

The LANGGRAPH_API_KEY environment variable isn’t set. Set via the Heroku GUI or run:

heroku config:set LANGGRAPH_API_KEY=your-secret-key -a your-app-name

Make sure your auth proxy handles streaming correctly. The /stream path detection is case-sensitive. Also check:

  • The httpx client timeout is long enough (300 seconds for long generations)

  • You’re using StreamingResponse not Response for stream endpoints

Check that:

  • The API key matches exactly (no extra whitespace or newlines)

  • The header name is correct (X-Api-Key or Authorization: Bearer)

  • Environment variables are set in both frontend and backend

  • The frontend isn’t caching an old API key (restart the Next.js dev server)

The entrypoint conditionally adds ssl_cert_reqs=none only for rediss:// URLs. Local Redis uses redis:// so it won’t have SSL issues. If you’re seeing SSL errors locally, check that your REDIS_URLstarts with redis:// not rediss://.

Make sure auth_proxy is in py-modules in your pyproject.toml:

[tool.setuptools]
py-modules = ["agent", "auth_proxy"]

Heroku expects one process per dyno that binds to $PORT. We work around this by:

  1. Starting LangGraph in the background on port 8000

  2. Running the auth proxy in the foreground on $PORT

When Heroku sends a SIGTERM to shut down the dyno, the auth proxy (running with exec) receives it directly. The backgrounded LangGraph process will be terminated when the container stops.

This is a simple API key authentication suitable for:

  • Internal tools

  • Prototypes

  • Small deployments with trusted clients

For production applications with end users, consider:

  • Rate limiting (add middleware to the Starlette app)

  • Request logging

  • Key rotation mechanisms

  • OAuth/JWT for user-level authentication

  • IP allowlisting if you know your frontend’s IP range

Adding authentication to a self-hosted LangGraph deployment requires:

  1. An auth proxy that validates API keys before forwarding to LangGraph

  2. Streaming support for Server-Sent Events endpoints

  3. Dual header support for compatibility with LangGraph SDK

  4. Consistent environment variables across frontend and backend

The proxy adds minimal latency (it runs on the same dyno) and keeps your LangGraph agent secure without requiring a LangGraph Cloud license.

Read the original on theagentnativeproduct.substack.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.