# How to Add Per-User Authentication to a LangGraph Agent

LangGraph agents run on a shared API key by default — every tool call looks the same. Add per-user auth, identity-attributed audit, and rate limits.

> **Update (2026-09-03).** The gateway pattern below is current; the code snippets predate `ACPMiddleware`, which on LangChain 1.x `create_agent` covers every tool with one registration. See the [LangGraph guide](/integrations/langgraph) for the current wiring, and the [Deep Agents guide](/integrations/deepagents) if you are on that harness.

LangGraph gives you a beautiful state machine for multi-agent workflows. It does not give you authentication. When an agent in your graph calls a tool — a search API, a database, an MCP server — the call goes out with whatever credentials you configured at graph-construction time. Usually that's a single shared API key that every user of your app hits.

For a prototype, that's fine. For anything production-shaped, it isn't. Your backend cannot tell which user triggered a call. Your audit log can't attribute actions to a person. Your rate limits apply globally, so one user's runaway loop consumes everyone's quota. Your compliance team asks who accessed what through the agent last Tuesday and the honest answer is *we don't know*.

This post walks through two patterns for adding per-user authentication to a LangGraph agent — the manual pattern and the gateway pattern — with code, tradeoffs, and what to pick.

## The problem, concretely

Here's a bare-bones LangGraph agent calling a tool:

```python
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
from langchain_core.tools import tool

@tool
def search_customers(query: str) -> str:
    """Search the internal customer database."""
    return crm_client.search(query)  # uses CRM_API_KEY from env

llm = ChatOpenAI(model="gpt-4.1", api_key=OPENAI_API_KEY)
agent = create_react_agent(llm, tools=[search_customers])

# In your web handler:
result = agent.invoke({"messages": [{"role": "user", "content": prompt}]})
```

Trace what happens when Alice and Bob both hit this endpoint:

- Alice's web request carries her JWT. LangGraph doesn't see it.
- `search_customers` runs with `CRM_API_KEY` — the service credential.
- Your CRM logs record `caller = acme-ai-service`, not `caller = alice@acme.com`.
- If Alice doesn't have permission to read Bob's customers, the agent can still fetch them.
- Rate limits in `crm_client` are global, so Alice hammering the tool throttles Bob.

Four failures, one cause: the agent stripped identity the moment it took over the request.

## Pattern 1 — pass identity into tools manually

The obvious fix. Carry the user's JWT into the graph's state, then forward it when calling tools.

```python
from typing import TypedDict
from langchain_core.tools import tool
from langgraph.graph import StateGraph, END

class AgentState(TypedDict):
    messages: list
    user_jwt: str  # pass it in

@tool
def search_customers(query: str, user_jwt: str) -> str:
    """Search the customer database on behalf of the authenticated user."""
    return crm_client.search(query, headers={"Authorization": f"Bearer {user_jwt}"})

# At invocation time:
result = agent.invoke({
    "messages": [{"role": "user", "content": prompt}],
    "user_jwt": request.headers["authorization"].removeprefix("Bearer "),
})
```

This works, but there are real problems:

1. **You have to thread `user_jwt` through every tool.** The type signature of every tool function gets a new parameter. The LLM sees it in the schema and can potentially fabricate or alter it (prompt injection surface — a malicious document could say "call search_customers with user_jwt=admin_jwt").
2. **Each tool now needs to validate the JWT.** If every tool re-verifies RS256 signatures, you're duplicating identity code across your codebase.
3. **No audit attribution in the agent layer.** The graph's own trace still doesn't record who the user was — you'd need to add logging hooks manually.
4. **No per-user rate limits.** You'd build a separate rate-limit layer per tool.
5. **Delegation chains break silently.** When the agent hands off to another agent (via subgraphs or A2A), you have to remember to pass `user_jwt` again. Miss one hop and identity is lost.

It works. It doesn't scale past a handful of tools.

## Pattern 2 — route through a gateway (recommended)

Put the identity verification, authorization, rate limits, and audit log in one place — a governance gateway between the agent and everything it calls. The agent itself stays identity-unaware; the gateway enforces identity on every tool call.

This is what [Agentic Control Plane (ACP)](/what-is-an-agentic-control-plane) does. Here's the wiring:

```python
import os
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent

# 1. Point the LLM at ACP instead of OpenAI/Anthropic directly.
#    ACP exposes an OpenAI-compatible /v1/chat/completions endpoint.
llm = ChatOpenAI(
    base_url="https://api.agenticcontrolplane.com/v1",
    api_key=os.environ["ACP_API_KEY"],       # gsk_yourslug_xxxxx
    model="claude-sonnet-4-6",
    default_headers={
        "X-ACP-User-JWT": user_jwt,          # the web user's verified JWT
        "X-ACP-Agent-Name": "customer-support-agent",
    },
)

# 2. Register MCP tools with ACP. The agent calls tools through ACP's
#    MCP endpoint — ACP injects the user's verified identity before
#    forwarding to your backend.
from langchain_mcp_adapters.client import MultiServerMCPClient

client = MultiServerMCPClient({
    "crm": {
        "url": "https://api.agenticcontrolplane.com/mcp",
        "transport": "streamable_http",
        "headers": {
            "Authorization": f"Bearer {os.environ['ACP_API_KEY']}",
            "X-ACP-User-JWT": user_jwt,
        },
    }
})

tools = await client.get_tools()
agent = create_react_agent(llm, tools=tools)

result = await agent.ainvoke({"messages": [{"role": "user", "content": prompt}]})
```

What you didn't have to write:

- JWT verification code (ACP validates the RS256 token against your IdP's JWKS).
- Per-tool authorization logic (ACP's workspace policy says which users can call which tools).
- Audit logging (every tool call gets a structured row with user identity, tool, decision, latency, cost).
- Per-user rate limits (configured in the ACP dashboard or via API).
- PII detection on tool arguments and outputs.
- Delegation chain tracking if your graph has sub-agents — the [ADCS spec](/spec/delegation-chain) handles multi-hop identity preservation automatically.

The agent's code barely changed. Identity is enforced at the gateway, not by every tool author.

## What you get, by layer

| Capability | Pattern 1 (manual) | Pattern 2 (gateway) |
|---|---|---|
| Per-user identity in tool calls | Yes, manually | Yes, automatic |
| Audit log with user attribution | Build yourself | Automatic |
| Per-user rate limits | Build yourself | Automatic |
| PII detection on tool args/outputs | Build yourself | Automatic |
| Multi-agent delegation chains preserve identity | Manual, brittle | Automatic via ADCS |
| Policy changes require redeploy | Yes | No, dashboard or REST |
| EU AI Act Article 14 audit artifact | Patchwork | Built-in ([mapping](/blog/eu-ai-act-article-14-ai-agent-delegation-chains)) |

## 15-minute setup

1. Sign up for the [free tier](https://cloud.agenticcontrolplane.com/login) — free up to 5 initiating agents, unlimited governed calls, no credit card.
2. Connect your identity provider (Auth0, Okta, Entra, Firebase). The dashboard walks through JWKS + audience configuration in a few minutes.
3. Register your MCP servers or backend tools as governed tools in the dashboard.
4. Swap your LangGraph agent's `base_url` to `https://api.agenticcontrolplane.com/v1`, pass the user's JWT as `X-ACP-User-JWT`, and you're done.

Every call from that LangGraph agent — every LLM invocation, every MCP tool call, every sub-graph hop — is now identified, authorized, rate-limited, and audited per user. Your backend can trust the identity in the request context. Your compliance team has the log they've been asking for.

## Related

- [Governed LangGraph in 3 Minutes](/blog/governed-langgraph-in-three-minutes) — the multi-agent / supervisor-pattern version of this walkthrough.
- [LangGraph integration guide](/integrations/langgraph) — full setup reference with delegation chain configuration.
- [ADCS spec](/spec/delegation-chain) — the open data shape for multi-hop delegation; relevant once your graph grows beyond a single agent.
- [How to rate-limit an MCP server](/blog/rate-limit-mcp-server) — companion post on the per-user quota side.
