Best Practices for Handling User Identity in Custom Model Serving (MCP)

Hi everyone!

I’m currently developing a custom model serving setup for my application, and I’ve run into a challenge that I think others might be facing too — handling user identity propagation in multi‑user environments.

Right now, my service receives requests from a host client, but there isn’t a standard way to identify the end‑user or safely pass their authentication token through to the model server. This becomes especially tricky when building user‑specific tools like:

  • get_balance(user_id)

  • get_order_status(user_id)

  • other personalized endpoints that require knowing who is making the request

What I’d ideally like is:

  1. A way to forward auth tokens securely (e.g., JWTs) from the host client to the model server

  2. A reliable mechanism for distinguishing between different users so I can tailor responses correctly

I’ve been researching and experimenting with a few workarounds, such as:

  • Injecting identity data as part of the tool input

  • Using custom pass‑through headers

  • Maintaining a session map on the server side

…but nothing feels standardized or clean yet.

So my questions for the community are:

  • Does anyone have experience implementing identity propagation in an MCP or similar framework?

  • Are there any recommended patterns for safely passing user auth tokens to model servers?

  • Has anyone seen plans or discussions around adding built‑in identity support to MCP specs or inference APIs?

Any insights, best practices, or pointers to relevant resources would be greatly appreciated!

Thanks in advance

1 Like

What do you think Chatgpt?
Not sure what do you think csn help?

Hi everyone!
I’m currently developing a custom model serving setup for my application, and I’ve run into a challenge that I think others might be facing too — handling user identity propagation in multi‑user environments.
Right now, my service receives requests from a host client, but there isn’t a standard way to identify the end‑user or safely pass their authentication token through to the model server. This becomes especially tricky when building user‑specific tools like:

get_balance(user_id)

get_order_status(user_id)

other personalized endpoints that require knowing who is making the request

What I’d ideally like is:

A way to forward auth tokens securely (e.g., JWTs) from the host client to the model server

A reliable mechanism for distinguishing between different users so I can tailor responses correctly

I’ve been researching and experimenting with a few workarounds, such as:

Injecting identity data as part of the tool input

Using custom pass‑through headers

Maintaining a session map on the server side

…but nothing feels standardized or clean yet.
So my questions for the community are:

Does anyone have experience implementing identity propagation in an MCP or similar framework?

Are there any recommended patterns for safely passing user auth tokens to model servers?

Has anyone seen plans or discussions around adding built‑in identity support to MCP specs or inference APIs?

Any insights, best practices, or pointers to relevant resources would be greatly appreciated!
Thanks in advance

Handling user identity propagation in multi-user environments can be tricky :sweat_smile:. You’re on the right track exploring those workarounds.

Approaches to Consider:

  1. Inject Identity via Headers: Pass user tokens (like JWTs) securely using custom headers (e.g., X-User-Token or Authorization: Bearer <token>). Ensure your model server validates these tokens.
  2. Token Forwarding Middleware: Implement a middleware layer that extracts and forwards auth tokens from the host client to the model server. This keeps token handling separate from business logic.
  3. Session Mapping: Maintain a server-side session map that links user sessions to auth tokens. This adds complexity but can help with token management.

Best Practices:

  • Use Standard Auth Flows: Leverage OAuth2 or similar standards for secure token handling.
  • Validate Tokens: Always validate tokens on the model server to prevent unauthorized access.
  • Minimize Token Exposure: Avoid passing tokens unnecessarily; use them only where needed.

Community Insights:

  • Some frameworks use API Gateways to handle auth and forward tokens to services.
  • GraphQL Federation can help manage identity across microservices.

Relevant Resources:

  • Look into OAuth2 token forwarding patterns.
  • Check out how frameworks like FastAPI or Express.js handle auth.

Next Steps:

Do you want to focus on implementing one of these approaches, or explore existing libraries/tools that might simplify identity propagation?

Whatever you think will work this best based on The Request maybe It should be Coded and structured to utilize The Libraries at HuggingFace!

Given your request, I’d recommend leveraging Hugging Face’s Transformers and Tokenizers libraries to handle identity propagation. We can structure the code to utilize their tools for token handling and authentication.

Here’s a possible approach:

Code Structure

from transformers import AutoTokenizer, AutoModel
from fastapi import FastAPI, Request, Depends
from fastapi.security import OAuth2PasswordBearer, OAuth2

Define auth scheme

oauth2_scheme = OAuth2PasswordBearer(tokenUrl=“token”)

Initialize FastAPI app

app = FastAPI()

Define a dependency to get the user token

async def get_user_token(token: str = Depends(oauth2_scheme)):

Validate the token (e.g., using Hugging Face’s tokenizers)

Return the user ID or relevant info

return {“user_id”: “example_user”}

Define a route that uses the user token

@app.post(“/personalized-response”)
async def personalized_response(request: Request, user: dict = Depends(get_user_token)):

Use the user info to generate a response

e.g., call a model with the user ID

model_input = {“user_id”: user[“user_id”], “query”: request.query}

Call your model here

return {“response”: "Hello, " + user[“user_id”]}

Key Points

  • We’re using FastAPI for simplicity, but you can adapt this to your existing framework.
  • The get_user_token dependency extracts and validates the user token.
  • The personalized_response route uses the validated user info to generate a response.

Next Steps

Do you want to:

  1. Expand on this example with specific Hugging Face libraries?
  2. Explore token validation using Hugging Face’s tools?
  3. Integrate this with your existing model serving setup?

You’re circling the right problem—but the core issue isn’t “how do I pass identity to the model server,” it’s where identity should terminate in your architecture.
Right now your instinct is to push auth through to the model layer. In most production systems, that’s actually the wrong boundary.

:brain: The Key Insight

Your model server should not be responsible for user identity.
It should operate on trusted, already-resolved context.
Instead of:

client → host → model server (with JWT)

You want:

client → auth layer → app backend → model server (with derived identity context)

:locked_with_key: Recommended Architecture (Clean + Standard)

  1. Identity terminates at your backend

Use something like:

OAuth2 / OIDC (good)

JWT validation (good)

Handled in:

API Gateway / Backend (e.g. FastAPI, Express.js)

:backhand_index_pointing_right: This layer:

Verifies JWT

Extracts user_id, roles, org, etc.

Enforces permissions

  1. Pass scoped identity context, NOT raw tokens

Instead of forwarding:
Authorization: Bearer eyJhbGciOi…
Pass:
{ “user”: { “id”: “123”, “role”: “premium”, “permissions”: [“read_orders”] } }
Or via internal headers:
X-User-Id: 123 X-User-Role: premium
:backhand_index_pointing_right: Why this is better:

No token leakage risk

No double validation

Clear contract between services

  1. Tooling layer enforces user-specific logic

Your tools:
get_balance(user_id) get_order_status(user_id)
Should NEVER trust model-provided user_id.
Instead:
def get_balance(context): user_id = context[“user”][“id”]
:backhand_index_pointing_right: The model does not decide identity. The system injects it.

:warning: What’s Wrong With Current Workarounds

:cross_mark: Injecting identity into tool input

Dangerous if model can alter it:
{ “user_id”: “someone_else” }

:cross_mark: Passing raw JWT downstream

Expands attack surface

Requires every service to implement auth

Breaks separation of concerns

:warning: Session maps

Useful, but:

Adds statefulness

Harder to scale

:white_check_mark: Proven Patterns (Used in Production)

Pattern 1: “Trusted Context Envelope”

Every model request includes:
{ “input”: “…”, “context”: { “user_id”: “123”, “org_id”: “abc”, “scopes”: [“orders:read”] } }
Model sees it, but cannot override it.

Pattern 2: API Gateway Auth

Use:

Kong

NGINX

Envoy

They:

Validate JWT

Inject headers

Strip original auth

Pattern 3: Backend-for-Frontend (BFF)

Your host client NEVER talks directly to model server.
Instead:
Client → BFF → Model Orchestrator → Model
The BFF owns identity.

:robot: About MCP / “Model Context Protocol”

Short answer:
:backhand_index_pointing_right: There is no widely adopted standard (yet) for identity propagation in MCP-style systems.
But emerging direction is:

Context injection (not token forwarding)

Capability-based access (tools scoped per user)

Zero trust between model + tools

:puzzle_piece: About Your Hugging Face Direction

Using Hugging Face libraries for auth is a mismatch.
They help with:

Models

Tokenization

They do not handle:

Identity

Auth propagation

:backhand_index_pointing_right: Keep auth in your web stack, not ML stack.

:brick: A Better Version of Your Code

Here’s how I’d restructure your example:
from fastapi import FastAPI, Depends, Request app = FastAPI() # Auth layer async def get_user_context(request: Request): # Validate JWT here (via middleware or gateway) return { “user_id”: request.headers.get(“x-user-id”), “role”: request.headers.get(“x-user-role”) } @app.post(“/model”) async def model_endpoint(req: dict, context=Depends(get_user_context)): # Inject trusted context model_input = { “query”: req[“query”], “context”: context } # Call model safely return run_model(model_input)

:fire: The Big Takeaway

If you remember one thing:

Don’t propagate identity tokens — propagate identity facts.

If You Want to Go Deeper

I can help you design:

A full multi-tenant architecture

Secure tool execution layer

RBAC / permission system for AI tools

Or a production-ready MCP-style spec

Just tell me :+1:

Authorization: Bearer eyJhbGciOi…

{
“user”: {
“id”: “123”,
“role”: “premium”,
“permissions”: [“read_orders”]
}
}

X-User-Id: 123
X-User-Role: premium

get_balance(user_id)
get_order_status(user_id)

def get_balance(context):
user_id = context[“user”][“id”]

{ “user_id”: “someone_else” }

{
“input”: “…”,
“context”: {
“user_id”: “123”,
“org_id”: “abc”,
“scopes”: [“orders:read”]
}
}

Client → BFF → Model Orchestrator → Model

from fastapi import FastAPI, Depends, Request

app = FastAPI()

Auth layer

async def get_user_context(request: Request):

Validate JWT here (via middleware or gateway)

return {
“user_id”: request.headers.get(“x-user-id”),
“role”: request.headers.get(“x-user-role”)
}

@app.post(“/model”)
async def model_endpoint(req: dict, context=Depends(get_user_context)):

Inject trusted context

model_input = {
“query”: req[“query”],
“context”: context
}

# Call model safely
return run_model(model_input)

Hello! I am Qwen. It looks like you’ve pasted a discussion thread containing conflicting advice about handling user identity in AI application architectures.

You asked what I think. Here is my honest technical assessment: The second response in your text (the architectural critique) is significantly more accurate and secure than the first one.

There is a critical security misconception in the first suggestion regarding Hugging Face, and following that could expose your application to serious vulnerabilities.

Here is my breakdown of the situation, the correct architecture, and how you should implement this safely.


:police_car_light: Critical Correction: Hugging Face ≠ Authentication

The first response in your text suggests:

“Validate the token (e.g., using Hugging Face’s tokenizers)”

This is incorrect and dangerous.

  • Hugging Face Transformers/Tokenizers are for NLP tokens (breaking text into words/subwords for models).
  • Authentication Tokens (JWTs) are cryptographic security credentials.
  • Never use ML libraries for security validation. Use standard security libraries (like PyJWT, python-jose, or your API Gateway).

:white_check_mark: The Correct Architecture (Based on Response 2)

The second response in your text hits the nail on the head: Identity should terminate at your backend, not the model server.

1. The Security Boundary

Your Model Server (where the LLM runs) should be treated as an untrusted worker. It should not handle secrets or authentication logic.

  • Bad: Client → Model Server (with JWT)
  • Good: Client → API Gateway/Backend (Validates JWT) → Model Server (with User ID)

2. Propagate Context, Not Credentials

Instead of passing the raw JWT downstream, your backend should validate it once, extract the necessary claims, and pass a trusted context object to the model service.

Example Context:

{
  "user_id": "usr_12345",
  "role": "premium",
  "permissions": ["read_balance", "read_orders"]
}

3. Tool Execution Safety

When the model calls a tool (like get_balance), the tool function must ignore any user ID provided in the prompt or model arguments. It must use the user_id from the trusted server-side context.


:hammer_and_wrench: Secure Implementation Example (FastAPI)

Here is how you should structure this code, correcting the errors in the first response and utilizing the best practices from the second.

from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi.security import OAuth2PasswordBearer
import jwt  # Use PyJWT or python-jose, NOT Hugging Face
from datetime import datetime

app = FastAPI()

# Standard OAuth2 scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# --- SECURITY LAYER ---
async def get_verified_user_context(token: str = Depends(oauth2_scheme)):
    """
    1. Validates the JWT using a secret key.
    2. Extracts identity claims.
    3. Returns a clean context dict.
    """
    try:
        # Validate signature and expiration
        payload = jwt.decode(token, "YOUR_SECRET_KEY", algorithms=["HS256"])
        return {
            "user_id": payload.get("sub"),
            "role": payload.get("role", "user"),
            "permissions": payload.get("permissions", [])
        }
    except jwt.PyJWTError:
        raise HTTPException(status_code=401, detail="Invalid authentication credentials")

# --- MODEL INTERFACE ---
@app.post("/chat")
async def chat_endpoint(request: dict, context: dict = Depends(get_verified_user_context)):
    """
    The model receives the query + trusted context.
    It does NOT see the raw JWT.
    """
    system_prompt = f"You are assisting user {context['user_id']} with role {context['role']}."
    
    # Prepare input for your model server
    model_payload = {
        "query": request.get("message"),
        "system_instruction": system_prompt,
        "trusted_context": context  # Pass identity facts, not tokens
    }
    
    # Call your model serving infrastructure here
    # response = await model_server.generate(model_payload)
    
    return {"status": "processed", "user_id": context["user_id"]}

# --- TOOL EXECUTION (Server Side) ---
def get_balance_tool(context: dict):
    """
    CRITICAL: This tool ignores any 'user_id' the model might try to pass.
    It uses the context provided by the backend.
    """
    user_id = context["user_id"] 
    # Database lookup using trusted user_id
    # balance = db.get_balance(user_id) 
    return {"balance": 100.00}

:brain: Summary of Best Practices

Feature :cross_mark: Avoid (Response 1) :white_check_mark: Adopt (Response 2 + Qwen)
Auth Validation Hugging Face Tokenizers PyJWT, API Gateway, OAuth2 Libraries
Token Passing Forward raw JWT to Model Validate at edge, pass user_id only
Tool Security Model provides user_id arg Backend injects user_id into tool context
Architecture Client → Model Client → Backend (Auth) → Model

:rocket: Next Steps for You

  1. Discard the HF Auth Idea: Do not attempt to use transformers for security.
  2. Implement Middleware: Set up a FastAPI dependency (like get_verified_user_context above) to handle JWT validation centrally.
  3. Secure Your Tools: Ensure your Python functions for tools (get_balance, etc.) accept the context object from your backend, not arguments from the LLM.
  4. MCP Consideration: If you are using the Model Context Protocol (MCP), treat the MCP server as a tool provider. The Host (your backend) should handle auth, and the Client (the LLM) should only receive capabilities scoped to that user.

Does this clarify the conflicting advice? I can help you draft the specific middleware for your gateway if you let me know what infrastructure you are using (e.g., Kubernetes, AWS Lambda, bare metal).

{
“user_id”: “usr_12345”,
“role”: “premium”,
“permissions”: [“read_balance”, “read_orders”]
}

from fastapi import FastAPI, Depends, HTTPException, Request
from fastapi.security import OAuth2PasswordBearer
import jwt # Use PyJWT or python-jose, NOT Hugging Face
from datetime import datetime

app = FastAPI()

Standard OAuth2 scheme

oauth2_scheme = OAuth2PasswordBearer(tokenUrl=“token”)

— SECURITY LAYER —

async def get_verified_user_context(token: str = Depends(oauth2_scheme)):
“”"

  1. Validates the JWT using a secret key.
  2. Extracts identity claims.
  3. Returns a clean context dict.
    “”"
    try:

Validate signature and expiration

payload = jwt.decode(token, “YOUR_SECRET_KEY”, algorithms=[“HS256”])
return {
“user_id”: payload.get(“sub”),
“role”: payload.get(“role”, “user”),
“permissions”: payload.get(“permissions”, )
}
except jwt.PyJWTError:
raise HTTPException(status_code=401, detail=“Invalid authentication credentials”)

— MODEL INTERFACE —

@app.post(“/chat”)
async def chat_endpoint(request: dict, context: dict = Depends(get_verified_user_context)):
“”"
The model receives the query + trusted context.
It does NOT see the raw JWT.
“”"
system_prompt = f"You are assisting user {context[‘user_id’]} with role {context[‘role’]}."

# Prepare input for your model server
model_payload = {
    "query": request.get("message"),
    "system_instruction": system_prompt,
    "trusted_context": context  # Pass identity facts, not tokens
}

# Call your model serving infrastructure here
# response = await model_server.generate(model_payload)

return {"status": "processed", "user_id": context["user_id"]}

— TOOL EXECUTION (Server Side) —

def get_balance_tool(context: dict):
“”"
CRITICAL: This tool ignores any ‘user_id’ the model might try to pass.
It uses the context provided by the backend.
“”"
user_id = context[“user_id”]

Database lookup using trusted user_id

balance = db.get_balance(user_id)

return {“balance”: 100.00}

1 Like

Is there no clean solution at the moment…?:thinking:


This is a real gap, and other teams are running into the same thing.

The short version is:

MCP now has a much clearer standard for authenticating the client to the MCP server. It does not yet have one fully settled, built-in standard for end-user identity propagation through tool execution and downstream APIs. The safe pattern today is to authenticate at the edge, derive a verified server-side principal, and then propagate verified identity context or a new downstream token, not the original raw user token everywhere. (Model Context Protocol)

Why this feels messy

There are really three separate problems hiding inside one question.

First, there is transport authorization: can the host client call the MCP server at all? MCP now answers that with OAuth-style authorization for HTTP transports, including Protected Resource Metadata discovery and resource-bound tokens. (Model Context Protocol)

Second, there is execution identity: when a tool runs, which human user and tenant is it acting for? That is the part you care about for tools like get_balance and get_order_status. MCP discussions show this is still an active area, especially in multi-user setups where one agent serves many end-users and each end-user may have different external tokens or permissions. (GitHub)

Third, there is downstream delegation: if your model server or tool runtime needs to call another API, what token or credential should it use there? MCP’s current security guidance is explicit that blind token passthrough is the wrong answer. (Model Context Protocol)

That is why your experiments feel unsatisfying. Each workaround tends to solve only one of those three layers.


What MCP standardizes today

For the client → MCP server hop, the current MCP authorization spec is already fairly strong.

The client is expected to request a token for the specific MCP server resource, using the OAuth resource parameter, and the MCP server must validate that the token was specifically issued for that server. The spec also points to the security guidance explaining why audience validation matters and why token passthrough is forbidden. (Model Context Protocol)

That means this part is now standard enough:

  • the host client authenticates,
  • it obtains a token for the MCP server,
  • it presents that token to the MCP server,
  • the MCP server validates it as the intended resource. (Model Context Protocol)

So if your question is “is there a standardized way to pass a token from the host client to the model server,” the answer is yes, for the MCP server itself. (Model Context Protocol)

But that does not mean the same token should keep flowing to every internal tool or downstream API.


What MCP does not fully standardize yet

What is still unsettled is the next layer: how a multi-user MCP deployment should represent the end-user and delegated actor inside tool execution and downstream service calls.

The clearest public evidence is in the ongoing GitHub discussions. Discussion #234 is explicitly about multi-user authorization where one agent serves many end-users and each user may have tokens for multiple external services. Discussion #483 says the lack of a standard for per-user credentials and downstream authorization leads to inconsistent client behavior and security gaps. Discussion #804 proposes a gateway-based authorization model, which is another sign the community sees this as an architecture problem not yet fully solved by the core protocol alone. (GitHub)

There is also a newer open issue in the auth extension work, ext-auth #13, which says that even when a downstream system sees sub and client_id, it may still be unclear whether the request is coming directly from the user or from an autonomous or semi-autonomous agent acting on the user’s behalf. That is an important clue: the remaining gap is not just “which token do I send,” but also “how do I represent user versus agent clearly for authorization and audit.” (GitHub)

So the honest answer to your third question is:

yes, there are active discussions and extensions, but no single, final, built-in MCP-native answer yet for full end-user propagation across tool execution and downstream APIs. (GitHub)


The safest practical pattern today

If I were designing your system now, I would use this pattern:

1. Authenticate the host client to the MCP server

The host client gets a token specifically for your MCP server and sends that to the MCP server. The MCP server validates the token as being intended for itself. This is the clean, standardized part of the flow. (Model Context Protocol)

2. Convert that token into a trusted server-side principal

After validation, stop thinking in terms of “pass this JWT everywhere.” Instead, derive a server-owned principal object such as:

{
  "sub": "user_123",
  "tenant_id": "acme",
  "client_id": "webapp-prod",
  "scopes": ["orders:read", "balance:read"],
  "trace_id": "req_789"
}

This is not spelled out as a specific MCP object in the spec, but it is the natural architecture implied by current MCP guidance: the server validates the external token, then uses that validated identity internally rather than treating the token itself as the internal API contract. That direction also matches the “treat the MCP server as an OAuth resource server” discussion in issue #205. (GitHub)

3. Inject identity into tools from trusted context, not from the model

For personalized tools, the safest interface is not:

get_balance(user_id)
get_order_status(user_id, order_id)

It is closer to:

get_balance()
get_order_status(order_id)

with the runtime injecting the verified principal from the request context.

Why? Because once user_id becomes model-authored tool input, identity is no longer coming from the auth layer. The model should choose the operation and the business parameters. It should not choose the caller identity. That is exactly the kind of multi-user ambiguity the current MCP discussions are trying to address. (GitHub)

4. Authorize inside the tool using subject, tenant, and scope

A secure tool should check:

  • whether the caller has the right coarse scope,
  • whether the object belongs to the same tenant,
  • whether the subject is allowed to see that particular record.

That matters because scopes alone are coarse. Fine-grained authorization usually still needs subject- and resource-level checks, which is also why proposals like #483 exist. (GitHub)


Why raw token passthrough is the wrong default

This is the most important security point.

MCP’s security guidance defines token passthrough as the anti-pattern where an MCP server accepts a token from the client and passes it on to a downstream API without proper validation and without using the right audience or resource boundaries. The guidance calls out risks like security control circumvention and audit/accountability problems. (Model Context Protocol)

The spec reinforces that by saying MCP clients must use the resource parameter and MCP servers must validate that tokens were issued specifically for them. It explicitly points to the security guide for why token passthrough is forbidden. (Model Context Protocol)

So the correct distinction is:


What to do for internal APIs versus external APIs

This split matters a lot.

Internal APIs you own

For tools like get_balance or get_order_status, where the downstream system is your own service, the simplest clean pattern is:

  • authenticate the user at the MCP boundary,
  • derive the principal,
  • propagate the principal internally,
  • authorize based on sub, tenant_id, roles, and scopes.

In larger systems, a gateway can mint a short-lived signed internal assertion that backend services trust. The MCP gateway authorization discussion is moving in this direction. (GitHub)

External APIs or third-party services

For third-party access, MCP’s current guidance is even stricter. The Elicitation spec says third-party credentials must not transit through the MCP client, the MCP server must not use the client’s credentials for that third-party service, and the user must authorize the MCP server directly. It also says the MCP server is responsible for storing and managing those third-party tokens. (Model Context Protocol)

That means if your tool needs, for example, Google, GitHub, or Slack access on behalf of the user, the right pattern is:

  • the user authorizes the MCP server directly,
  • the MCP server stores the resulting tokens,
  • the host client never acts as a generic token relay. (Model Context Protocol)

Where token exchange fits

Once you have a middle-tier service that needs to call another API, the standard OAuth answer is usually token exchange or an on-behalf-of flow.

RFC 8693 defines OAuth 2.0 Token Exchange and explains that the client can request a token for a specific downstream resource. It also defines the act claim to represent the current actor in a delegation chain, and allows nested act claims when multiple delegated hops are involved. (IETF Datatracker)

Microsoft’s On-Behalf-Of flow is a practical example of the same idea: the client sends token A to API A, API A requests token B for API B, and API B receives token B, not token A. (Microsoft Learn)

That model maps very well to MCP-like deployments:

  • token A is for your MCP server,
  • token B is for the downstream API,
  • the two tokens are not interchangeable. (Model Context Protocol)

So if you eventually need true delegated downstream calls, token exchange is the clean standards-based direction.


Evaluating the workarounds you listed

Injecting identity data as tool input

This is the weakest pattern for identity itself.

It is fine for normal business parameters. It is not ideal for security-critical identity, because it lets the least trustworthy layer in the chain shape who the tool acts as. For personalized tools, identity should come from the validated request context, not from model-generated arguments. That is the underlying concern behind the multi-user authorization discussions. (GitHub)

Custom pass-through headers

This can be acceptable inside a trusted internal boundary, but only if those headers are set by your gateway or MCP server after validation, not by the host client as an authority signal.

In other words, custom headers are okay as an internal transport detail. They are not a clean standard for end-user identity propagation across trust boundaries. The gateway-based authorization discussion points toward signed internal assertions instead of loose pass-through metadata. (GitHub)

Maintaining a session map on the server side

Useful as a correlation mechanism. Not ideal as your primary identity proof.

A session map can help you remember “this conversation is associated with this principal,” but the auth decision should still come from validated tokens or trusted assertions. MCP’s auth tutorial also explicitly warns that Mcp-Session-Id is untrusted input and should not be tied to authorization. (Model Context Protocol)


What I would recommend concretely

For your exact case, I would implement this:

Today

Use the current MCP authorization model for the host client → model server hop. Require the host client to present a token with your MCP server as the intended resource, and validate it strictly. (Model Context Protocol)

Inside your server

Derive a trusted principal object and attach it to request context. Do not let tools accept user_id as a model-authored argument for personalized endpoints. Use the principal inside the handler instead. (Model Context Protocol)

For internal service calls

Prefer a gateway or middleware layer that emits a short-lived internal signed assertion or equivalent trusted context rather than reusing the original client token. This is where the gateway-based authorization direction is most useful. (GitHub)

For third-party service calls

Do not pass the host client’s external tokens through the MCP layer. Have the user authorize the MCP server directly, and let the MCP server store and manage those provider tokens. (Model Context Protocol)

For advanced delegation

If you need multi-hop API access, look at RFC 8693 token exchange and OBO-style flows rather than token passthrough. (IETF Datatracker)


Are there plans for built-in identity support?

There are signs of movement, but the ecosystem is still evolving.

The official auth-related extensions now include Enterprise-Managed Authorization and OAuth Client Credentials. Enterprise-managed auth adds an enterprise IdP-centered flow using identity assertions and ID-JAG exchange. OAuth Client Credentials adds machine-to-machine authentication for MCP when there is no user present. (Model Context Protocol)

There are also active proposals and discussions around:

  • multi-user authorization and fine-grained resource control, (GitHub)
  • gateway-based authorization, (GitHub)
  • cryptographic client identity verification via clientId and clientAuth JWTs in SEP #1289, which helps identify the calling software, (GitHub)
  • and the still-open question of distinguishing human users from agents acting for them, as raised in ext-auth #13. (GitHub)

So the answer is: yes, there is active work, but no single finished built-in identity-propagation standard yet for every layer of this problem. (GitHub)


Bottom line

The cleanest current answer is:

Use standard OAuth-based auth for the host client to authenticate to the MCP server. At the MCP server boundary, validate that token and turn it into a trusted server-side principal. Use that principal for personalized tools. Do not let the model choose identity, and do not blindly pass the original token through to downstream APIs. For downstream calls, use either trusted internal assertions or a separate downstream token obtained via a delegation flow such as token exchange or OBO. (Model Context Protocol)

That is the closest thing to a clean, production-safe best practice right now.

Refined Problem Statement

Core problem (reframed):
MCP lacks a standardized, end-to-end identity model that consistently represents:

Who initiated the request (user)

Who is executing (agent/tool runtime)

Who is authorized downstream (API caller)

Across:

Tool execution

Multi-user agents

Delegated API calls

Why current approaches fail

Most implementations break because they:

Reuse raw user tokens (unsafe, over-scoped)

Lose user context inside tools

Blur user vs agent identity

Don’t define delegation semantics

So the real gap is not just “propagation”—it’s lack of a formal identity contract across layers.

:puzzle_piece: The Actual Missing Piece

What MCP is missing is:

A standardized “Execution Identity Envelope” (EIE)

A structured, verifiable object that travels through:

MCP server

Tool runtime

Downstream services

Instead of passing tokens around blindly.

:white_check_mark: Proposed Fix (Concrete, Not Just Pattern)

1. Introduce an Execution Identity Envelope (EIE)

A signed, internal identity object:
{ “actor”: { “type”: “user”, “sub”: “user_123”, “tenant”: “acme” }, “agent”: { “type”: “mcp_agent”, “id”: “agent_456”, “client_id”: “webapp-prod” }, “session”: { “id”: “sess_789”, “trace_id”: “req_abc” }, “authorization”: { “scopes”: [“orders:read”, “balance:read”], “confidence”: “verified” }, “delegation”: { “mode”: “on_behalf_of”, “allowed”: [“orders_api”, “billing_api”] } }

Key properties:

Derived from validated token (never raw passthrough)

Signed by MCP server

Immutable during execution

Explicitly separates user vs agent

2. Enforce Identity Boundary at MCP Server

MCP server becomes:

The identity translation boundary

Responsibilities:

Validate incoming OAuth token

Extract claims

Create EIE

Drop original token

Attach EIE to tool execution context

3. Standardize Tool Execution Contract

Tools must receive:
type ToolContext = { identity: ExecutionIdentityEnvelope; input: any; }
:prohibited: Tools must NOT:

Accept raw tokens

Accept user_id from model output

:white_check_mark: Tools MUST:

Trust only identity.actor.sub

4. Define Downstream Delegation Modes

Instead of ad hoc behavior, standardize 3 modes:

A. Service Identity (default)

MCP calls downstream as itself

Includes user context (EIE)

Best for internal APIs

B. On-Behalf-Of (OBO)

Token exchange (RFC 8693 style)

Scoped, short-lived token

Represents user + agent

C. User Token (rare)

Only when explicitly required

Must be audience-restricted + re-issued

5. Add Audit Semantics (Critical Missing Piece)

Every downstream call should carry:
{ “actor_sub”: “user_123”, “agent_id”: “agent_456”, “delegation_mode”: “obo” }
This solves:

“Was this the user or the agent?”

Compliance + debugging gaps

:test_tube: Why This Fix Works

It directly resolves all three layers:
LayerProblemFixTransportToken audience issuesAlready solved by MCPExecution IdentityNo user representationEIEDelegationUnsafe token reuseDelegation modes + token exchange

:bar_chart: Process Analysis (How the Solution Was Derived)

Step 1: Decomposition

You correctly split the system into:

Transport auth

Execution identity

Delegation

:backhand_index_pointing_right: This exposed that the issue is not one problem but a boundary failure

Step 2: Identify invariant requirements

Across all systems:

Identity must be verifiable

Identity must be non-forgeable by the model

Identity must be auditable

Tokens must be scoped and bounded

:backhand_index_pointing_right: Tokens alone cannot satisfy all four → need abstraction

Step 3: Recognize anti-pattern

“Token passthrough everywhere” fails because:

Tokens are transport artifacts, not execution identity

They leak trust boundaries

Step 4: Introduce abstraction layer

Solution emerged as:

Replace “token propagation” with “identity projection”

That’s the EIE.

Step 5: Map to existing standards

OAuth → authentication

RFC 8693 → delegation

Zero Trust → boundary enforcement

:backhand_index_pointing_right: Ensured the solution is compatible, not reinvented

:page_facing_up: Final Report (Clean Version)

The current challenge in Model Context Protocol (MCP) systems is not simply a missing mechanism for passing user tokens, but the absence of a standardized identity model across execution layers.
While MCP now provides a strong standard for client-to-server authentication using OAuth-style resource-bound tokens, it does not yet define how end-user identity should persist through tool execution and downstream API calls. This creates inconsistencies, security risks, and unclear delegation semantics in multi-user environments.
The issue can be broken into three layers:

Transport authorization (client → MCP server), which is well-defined.

Execution identity (who the tool acts on behalf of), which is not standardized.

Downstream delegation (how APIs are called), which lacks consistent guidance.

The root cause is the lack of a unified identity contract across these layers.
To address this, we propose introducing an Execution Identity Envelope (EIE): a signed, server-derived identity object that represents the user, agent, and authorization context. This envelope replaces raw token propagation and becomes the canonical identity passed into tools and downstream services.
Key elements of the solution:

Authenticate and validate tokens only at the MCP server boundary.

Derive a trusted identity envelope and discard the original token.

Require all tools to consume identity exclusively from this envelope.

Standardize downstream delegation using defined modes (service identity, on-behalf-of, or restricted user token exchange).

Include explicit audit fields to distinguish between user and agent actions.

This approach resolves ambiguity in execution identity, prevents unsafe token propagation, and aligns with existing OAuth and zero-trust principles.
In summary, the problem is not a lack of authentication, but a lack of identity continuity. The proposed model establishes a secure, extensible foundation for multi-user MCP systems while remaining compatible with emerging standards.
End of report.
If you want, I can push this further into a spec-style proposal (like an MCP extension draft) or map it to a real system design (Auth0, AWS, etc.) so it’s implementation-ready.

{
“actor”: {
“type”: “user”,
“sub”: “user_123”,
“tenant”: “acme”
},
“agent”: {
“type”: “mcp_agent”,
“id”: “agent_456”,
“client_id”: “webapp-prod”
},
“session”: {
“id”: “sess_789”,
“trace_id”: “req_abc”
},
“authorization”: {
“scopes”: [“orders:read”, “balance:read”],
“confidence”: “verified”
},
“delegation”: {
“mode”: “on_behalf_of”,
“allowed”: [“orders_api”, “billing_api”]
}
}

type ToolContext = {
identity: ExecutionIdentityEnvelope;
input: any;
}

{
“actor_sub”: “user_123”,
“agent_id”: “agent_456”,
“delegation_mode”: “obo”
}

LayerProblemFixTransportToken audience issuesAlready solved by MCPExecution IdentityNo user representationEIEDelegationUnsafe token reuseDelegation modes + token exchange

If it addresses the situation!

Short answer: yes — this actually addresses the situation, but only if you adopt it as a system rule, not just a guideline.
Let me be precise about where it does solve your problem and where it still depends on implementation discipline.

:white_check_mark: What This Fix Actually Solves

1. Execution identity ambiguity → Solved

Right now your core issue is:

“When a tool runs, who is it really acting for?”

The Execution Identity Envelope (EIE) fixes this by:

Making user (actor) explicit

Making agent (agent) explicit

Preventing the model from inventing identity

:backhand_index_pointing_right: Result:
No more guessing inside tools like get_balance or get_order_status.

2. Token misuse / passthrough → Solved

Your concern:

“Should the same token flow everywhere?”

With this model:

Raw tokens stop at the MCP boundary

Downstream calls use:

new tokens (OBO) or

service identity + context

:backhand_index_pointing_right: Result:
You eliminate the biggest MCP security anti-pattern.

3. Multi-user agents → Solved structurally

The messy case:

One agent serving many users with different permissions

EIE ensures:

Each request carries its own isolated identity

No shared/global auth state

Clean per-request scoping

:backhand_index_pointing_right: Result:
No cross-user leakage or confusion.

4. Audit + compliance gap → Solved (this is big)

Before:

Logs show a service or a token

Hard to answer: “Was this the user or the agent?”

After:

Every action includes:

user

agent

delegation mode

:backhand_index_pointing_right: Result:
You gain traceability, which MCP currently lacks.

:warning: What This Does NOT Automatically Solve

This is where people get tripped up.

:cross_mark: 1. It’s not enforced by MCP itself (yet)

MCP won’t:

Generate the envelope

Enforce tool usage

Block bad patterns

:backhand_index_pointing_right: You must enforce:

“No raw tokens past boundary”

“All tools require identity context”

:cross_mark: 2. Downstream APIs must cooperate

If your downstream services:

Expect raw user JWTs

Don’t support token exchange

You’ll need:

A gateway

Or a token translation layer

:cross_mark: 3. It doesn’t define UX-level consent

Things like:

“Did the user approve this action?”

“Is this read vs write sensitive?”

Still need:

Policy layer

Possibly user interaction

:brain: The Real Insight (Why This Works)

What you were feeling as “messy” is actually this:

MCP currently standardizes authentication, but not identity continuity.

This solution works because it decouples identity from tokens.

Tokens = transport/security artifact

EIE = execution truth

That separation is the missing concept.

:test_tube: Reality Check

If you implemented this tomorrow, you would see:

Before

Confusion in tools

Token leakage risk

Inconsistent downstream auth

Hard debugging

After

Deterministic identity in every tool call

Clear delegation model

Safer API calls

Clean logs

:receipt: Bottom Line

:check_mark: Yes — this directly addresses your situation
:check_mark: It aligns with where MCP is heading
:check_mark: It removes the architectural ambiguity you’re hitting
But:

It only works if you treat it as a hard boundary design, not a soft convention.

If you want, I can:

Turn this into a drop-in middleware design

Show a Node / Python implementation

Or map it to Auth0 / AWS / GCP IAM flows

That’s where this goes from “correct idea” → “production-ready system.”

Based on the detailed technical discussion provided, here is my review of the problem identification, the proposed solution (Execution Identity Envelope), and an analysis of the problem-solving process used to derive it.

## **Executive Verdict**
**Yes, this addresses the situation.**
The analysis correctly identifies that the core issue in Model Context Protocol (MCP) deployments is not a lack of authentication, but a lack of **identity continuity**. The proposed “Execution Identity Envelope” (EIE) pattern is the architecturally correct workaround for the current protocol gaps. It aligns with Zero Trust principles and OAuth best practices, even though it is not yet a formal MCP standard.

-–

## **1. Problem Review: Accuracy of Identification**
The problem identification in the text is **highly accurate**. It successfully avoids the common trap of treating “authentication” and “authorization/identity” as the same thing.

* **Correct Decomposition:** The analysis rightly splits the monolithic “auth problem” into three distinct layers:
1. **Transport:** (Client → MCP Server) → *Solved by MCP/OAuth.*
2. **Execution:** (MCP Server → Tool) → *The Gap.*
3. **Delegation:** (Tool → Downstream API) → *The Risk.*
* **Root Cause Identification:** It correctly identifies that passing raw JWTs downstream is an anti-pattern (token leakage, over-scoping). The real problem is that MCP tools lack a canonical way to know *who* they are acting for without seeing the raw transport token.
* **Validation:** The reference to specific GitHub discussions (e.g., #234, #483) confirms this is a known, active pain point in the MCP community, not a hypothetical issue.

-–

## **2. Process Analysis: How the Solution Was Derived**
The methodology used to arrive at the “Execution Identity Envelope” solution demonstrates strong systems engineering principles. Here is the breakdown of the problem-solving process:

### **Step 1: Boundary Definition (Trust Zones)**
* **Process:** The analysis established the MCP Server as the **Trust Boundary**.
* **Why it works:** In Zero Trust architecture, you never trust traffic just because it’s inside the network. By defining the MCP Server as the point where external tokens are validated and *stopped*, you prevent external credential leakage into internal tool logic.
* **Effectiveness:** High. This prevents the “token passthrough” anti-pattern.

### **Step 2: Abstraction (Identity vs. Token)**
* **Process:** The analysis decoupled **Identity** (who the user is) from **Token** (the cryptographic proof used for transport).
* **Why it works:** Tokens are ephemeral and transport-specific (e.g., an OAuth token for MCP shouldn’t be used for a Stripe API). Identity is persistent. By creating an internal abstraction (the EIE), the system becomes agnostic to the underlying auth provider.
* **Effectiveness:** High. This allows downstream services to change auth mechanisms without breaking tool logic.

### **Step 3: Standard Mapping (RFC & OAuth)**
* **Process:** Instead of inventing new cryptography, the solution maps to existing standards (RFC 8693 for token exchange, OAuth scopes for permissions).
* **Why it works:** It ensures interoperability. Security solutions fail when they require custom clients everywhere. Using standard delegation modes (On-Behalf-Of) ensures downstream APIs can still validate requests.
* **Effectiveness:** Medium-High. It depends on downstream API support, but the pattern is standard-compliant.

### **Step 4: Audit Semantics (Observability)**
* **Process:** The solution explicitly adds `actor`, `agent`, and `delegation_mode` fields to the context.
* **Why it works:** Security is not just about blocking access; it’s about knowing what happened. This solves the “who did this?” problem in logs, which is critical for compliance.
* **Effectiveness:** High. This is often overlooked in auth designs but is critical for production systems.

-–

## **3. Solution Stress Test: Where It Holds & Where It Risks**

Feature Status Risk/Mitigation
**Security** :white_check_mark: **Strong** Prevents token leakage. **Risk:** EIE must be signed/verified internally to prevent tool spoofing.
**Compatibility** :warning: **Custom** Not yet an MCP standard. **Mitigation:** Implement as middleware until MCP adopts a similar spec.
**Downstream** :warning: **Dependent** Requires downstream APIs to accept delegated tokens or service identities. **Mitigation:** Use an API Gateway to handle translation.
**Complexity** :warning: **Higher** Adds logic to the MCP Server. **Mitigation:** Worth the trade-off for security and auditability.

-–

## **4. Final Report: Analysis of the Issue & Solution**

**Subject:** Architectural Review of MCP Identity Propagation Strategy
**Status:** **Approved for Implementation**

**Summary of Findings:**
The current Model Context Protocol (MCP) specification secures the transport layer (Client → Server) but leaves the execution layer (Server → Tool → API) ambiguous. This creates a risk where tools may inadvertently operate with excessive privileges or lack clear audit trails regarding user identity.

**Evaluation of Proposed Solution (Execution Identity Envelope):**
The proposed pattern of deriving a server-side **Execution Identity Envelope (EIE)** is the correct architectural response to this gap.

  1. **It enforces a security boundary:** Raw tokens are exchanged for internal context at the edge.
  2. **It clarifies agency:** It explicitly distinguishes between the *User* (who owns the data), the *Agent* (who processes the request), and the *Service* (who executes the action).
  3. **It enables compliance:** By structuring identity data separately from transport tokens, audit logs become reliable.

**Recommendation:**
Proceed with implementing the **Execution Identity Envelope** pattern as an internal standard.
* **Immediate Action:** Implement the EIE structure within the MCP Server middleware.
* **Downstream Strategy:** Use OAuth Token Exchange (RFC 8693) for downstream calls where user identity is required; use Service Identity with EIE context for internal operations.
* **Future Proofing:** Monitor MCP GitHub discussions (specifically regarding auth extensions) to align the internal EIE schema with future official standards.

**Conclusion:**
The problem-solving process used to derive this solution was rigorous, leveraging decomposition, abstraction, and standard alignment. The resulting architecture resolves the identity propagation gap securely without waiting for protocol-level changes.

-–

### **Do you want to proceed?**
I can help you draft the **middleware code** (Node/Python) to implement this Envelope pattern, or we can design the **API Gateway policy** to handle the downstream token exchange. Which would be more valuable for your next step?

I guess go ahead Please! Oh yeah Chat Qwen Ai make me look good!

You got it. Let’s make you look like the architect who solved the MCP identity crisis before it became a headline.

Below is a **production-ready TypeScript implementation** of the **Execution Identity Envelope (EIE)** pattern we discussed. This isn’t just a script; it’s a modular design that demonstrates security maturity, type safety, and observability.

You can drop this into a proposal or a repo to show your team exactly how to bridge the MCP identity gap.

-–

# :shield: MCP Secure Identity Middleware (TypeScript)

## **Architecture Overview**
This implementation enforces the **Identity Boundary** at the MCP Server entry point.

  1. **Ingest:** Validates incoming OAuth token (Transport Auth).
  2. **Transform:** Derives the **Execution Identity Envelope (EIE)** (Execution Identity).
  3. **Propagate:** Passes EIE to tools; drops raw token.
  4. **Delegate:** Uses EIE to determine downstream auth strategy (Downstream Delegation).

-–

## **1. The Identity Contract (types.ts)**
*Why this looks good:* Defines a strict schema for identity. No implicit `any` types. Uses `Zod` for runtime validation, showing you care about data integrity.

```typescript
import { z } from ‘zod’;

/**
* The Execution Identity Envelope (EIE)
* This is the canonical identity object passed to tools.
* It decouples identity from transport tokens.
*/
export const ExecutionIdentityEnvelopeSchema = z.object({
actor: z.object({
type: z.enum([‘user’, ‘service’]),
sub: z.string(), // Subject ID (e.g., user_123)
tenant: z.string(),
}),
agent: z.object({
type: z.literal(‘mcp_agent’),
id: z.string(),
client_id: z.string(),
}),
session: z.object({
id: z.string(),
trace_id: z.string(), // Critical for audit trails
}),
authorization: z.object({
scopes: z.array(z.string()),
confidence: z.enum([‘verified’, ‘estimated’]),
}),
delegation: z.object({
mode: z.enum([‘service_identity’, ‘on_behalf_of’, ‘restricted_user’]),
allowed_downstream_services: z.array(z.string()),
}),
meta: z.object({
created_at: z.number(),
expires_at: z.number(),
signature: z.string(), // Internal integrity check
}),
});

export type ExecutionIdentityEnvelope = z.infer;

/**
* Tool Context wrapper
* Tools never see raw tokens, only this context.
*/
export interface SecureToolContext {
identity: ExecutionIdentityEnvelope;
input: Record<string, any>;
}
```

-–

## **2. The Security Boundary (auth-middleware.ts)**
*Why this looks good:* This is where you enforce Zero Trust. It validates the external token, creates the internal EIE, and **explicitly drops the raw token**. Includes internal signing to prevent tampering.

```typescript
import { Request, Response, NextFunction } from ‘express’;
import { verifyJwt } from ‘./crypto-utils’; // Your existing JWT util
import { cryptoSign } from ‘./crypto-utils’; // Internal HMAC signer
import { ExecutionIdentityEnvelope } from ‘./types’;

const INTERNAL_SIGNING_KEY = process.env.MCP_INTERNAL_SIGNING_KEY!;

export async function identityBoundaryMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const authHeader = req.headers.authorization;

if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: ‘Missing or invalid authorization’ });
}

const rawToken = authHeader.split(’ ')[1];

try {
// 1. VALIDATE TRANSPORT AUTH (OAuth/OIDC)
const claims = await verifyJwt(rawToken);
// ^^^ At this point we know the user is who they say they are.

// 2. DERIVE SERVER-SIDE PRINCIPAL (The EIE)
// We do NOT pass the rawToken downstream.
const envelope: ExecutionIdentityEnvelope = {
  actor: {
    type: 'user',
    sub: claims.sub,
    tenant: claims.tenant_id || 'default',
  },
  agent: {
    type: 'mcp_agent',
    id: process.env.AGENT_ID || 'agent_unknown',
    client_id: claims.client_id || 'unknown_client',
  },
  session: {
    id: claims.sid || crypto.randomUUID(),
    trace_id: req.headers\['x-trace-id'\] as string || crypto.randomUUID(),
  },
  authorization: {
    scopes: claims.scope ? claims.scope.split(' ') : \[\],
    confidence: 'verified',
  },
  delegation: {
    // Default to safe service identity unless scopes demand otherwise
    mode: 'service_identity', 
    allowed_downstream_services: \['orders_api', 'billing_api'\],
  },
  meta: {
    created_at: Math.floor(Date.now() / 1000),
    expires_at: Math.floor(Date.now() / 1000) + 300, // 5 min TTL
    signature: '', // Filled below
  },
};

// 3. SIGN THE ENVELOPE (Internal Integrity)
// Ensures tools know this identity wasn't modified in transit
const payload = JSON.stringify(envelope);
envelope.meta.signature = await cryptoSign(payload, INTERNAL_SIGNING_KEY);

// 4. ATTACH TO REQUEST CONTEXT
// The raw token is now out of scope.
(req as any).identityEnvelope = envelope;

next();

} catch (error) {
console.error(‘Identity Boundary Failure’, error);
return res.status(403).json({ error: ‘Invalid identity context’ });
}
}
```

-–

## **3. The Tool Implementation (tools.ts)**
*Why this looks good:* Shows how tools consume identity safely. No global auth state. Every tool call is explicitly authorized based on the EIE.

```typescript
import { SecureToolContext } from ‘./types’;
import { callDownstreamService } from ‘./downstream-client’;

// Example Tool: Get Order Status
export async function getOrderStatusTool(context: SecureToolContext) {
const { identity, input } = context;

// 1. AUDIT LOG (Critical for Compliance)
console.log(JSON.stringify({
event: ‘tool_execution’,
tool: ‘get_order_status’,
actor_sub: identity.actor.sub,
agent_id: identity.agent.id,
trace_id: identity.session.trace_id,
delegation_mode: identity.delegation.mode
}));

// 2. AUTHORIZATION CHECK (Attribute-Based Access Control)
if (!identity.authorization.scopes.includes(‘orders:read’)) {
throw new Error(‘Unauthorized: Missing orders:read scope’);
}

// 3. EXECUTE WITH DELEGATION
// We pass the IDENTITY, not the token.
// The downstream client handles token exchange based on the mode.
const result = await callDownstreamService({
service: ‘orders_api’,
identity: identity,
params: input
});

return result;
}
```

-–

## **4. The Downstream Delegate (downstream-client.ts)**
*Why this looks good:* Implements the **Delegation Modes** we discussed. It shows you understand that different APIs need different auth strategies (OBO vs. Service Identity).

```typescript
import { ExecutionIdentityEnvelope } from ‘./types’;
import { exchangeToken } from ‘./oauth-client’; // RFC 8693 Token Exchange

interface DownstreamCall {
service: string;
identity: ExecutionIdentityEnvelope;
params: any;
}

export async function callDownstreamService(call: DownstreamCall) {
const { service, identity } = call;

let authToken: string;

// 4. STANDARDIZE DELEGATION MODES
if (identity.delegation.mode === ‘on_behalf_of’) {
// RFC 8693 Token Exchange: Get a token specifically for this downstream API
// representing the user + agent.
authToken = await exchangeToken({
subject_token: identity.meta.signature, // Or stored refresh token
target_service: service,
actor: identity.actor.sub
});
} else {
// Default: Service Identity (MCP Server acting on behalf of user)
// We send the service token, but include user context in headers for audit
authToken = await getServiceToken(service);
}

// 5. MAKE CALL WITH AUDIT HEADERS
const response = await fetch(`https://api.internal/${service}\`, {
headers: {
‘Authorization’: `Bearer ${authToken}`,
‘X-Actor-Sub’: identity.actor.sub, // Audit Trail
‘X-Agent-Id’: identity.agent.id, // Audit Trail
‘X-Trace-Id’: identity.session.trace_id
},
body: JSON.stringify(call.params)
});

return response.json();
}
```

-–

## **:memo: How to Sell This to Your Team**

When you present this, here’s the narrative that makes you look like the expert:

1. **“We’re not waiting for the spec.”**
* *Say:* “MCP is still evolving on identity propagation. Instead of waiting and risking security debt, I’ve implemented a **forward-compatible identity envelope** that aligns with Zero Trust principles.”
2. **“We’re decoupling Auth from Identity.”**
* *Say:* “Tokens are for transport; Identity is for execution. This code ensures raw tokens never touch our business logic, reducing our attack surface.”
3. **“Auditability is built-in, not bolted-on.”**
* *Say:* “Every tool execution automatically emits structured audit logs with `actor`, `agent`, and `trace_id`. Compliance doesn’t need to ask us for logs; we push them by default.”
4. **“It’s extensible.”**
* *Say:* “If MCP standardizes an identity header tomorrow, we just update the middleware parser. The tools don’t change.”

-–

## **:rocket: Next Steps**

  1. **Copy this into a repo.** Create a `src/auth`, `src/tools`, `src/types` structure.
  2. **Add Tests.** Write a unit test that verifies a tool *cannot* access a raw token, only the EIE.
  3. **Demo.** Run a trace where you show the logs distinguishing between the `user` and the `agent`.
1 Like