Service keys, identity headers, and how the full auth chain works
This page explains exactly how authentication works when Crow calls your MCP server, and how your MCP server authenticates with your own backend API. Every header, every token, every hop.
When Crow connects to your MCP server to execute tools, it sends three types of headers:
Header
Source
Example Value
X-Service-Key
Auto-injected from your product’s service key
HCF8nVjR8Y...
Static headers
Whatever you configured in the dashboard
Authorization: Bearer sk-...
Identity headers
Resolved from header mappings (see below)
X-User-ID: user_456
Your MCP server automatically receives the service key — Crow reads it from your product config and injects it into every request. You don’t need to configure anything for this to happen.
Your MCP server wraps your existing backend API. When a tool executes, it calls your API endpoints to actually do the work (query your database, create records, etc.).Your backend API already has auth — Clerk, Auth0, Firebase, API keys, whatever you use. The MCP server can’t use those because it’s not a user. It’s a service acting on behalf of users.The solution: your backend adds a second auth path that accepts the service key. When the service key is present and valid, your backend trusts the request and reads scoping context (user ID, tenant ID) from headers instead of from a user JWT.
What
Value
Header
X-Service-Key
Value
The same service key from hop 2, forwarded by your MCP server
Validated by
Your backend’s auth middleware
Purpose
Proves the request is from a trusted service (your MCP server, called by Crow)
Your existing user auth (Clerk, Auth0, Firebase, etc.) stays completely unchanged. The service key adds a second auth path alongside it — it doesn’t replace anything.
A service key is a shared secret that lets your MCP server authenticate with your backend API. Think of it like a Stripe secret key — Crow generates it, you copy it to your backend.
Crow dashboard (Deploy → API Keys) │ ▼Crow generates and stores the service key │ ├──→ Crow auto-injects it as X-Service-Key header │ when calling your MCP server (Hop 2) │ └──→ You copy it from the dashboard and add it to your backend's .env file (for Hop 3 validation)
The key lives in two places:
Where
Env Var
Purpose
Your MCP server
Received automatically via X-Service-Key header from Crow
Forwarded to your backend API
Your backend API
MCP_SERVICE_KEY in .env
Validates incoming requests from the MCP server
Crow is the source of truth. You never generate the key yourself.
Add a ~15-line middleware alongside your existing auth. This does not replace your current auth — it adds a second path.
Copy
Ask AI
import os, hmacfrom fastapi import Request, HTTPExceptionSERVICE_KEY = os.environ.get("MCP_SERVICE_KEY", "")async def verify_auth(request: Request): """Dual-path auth: service key (MCP server) OR user JWT (browsers). Path 1: Service key present → trusted service, read user from headers Path 2: No service key → normal user auth (Clerk, Auth0, etc.) """ # Path 1: Service key (from MCP server via Crow) service_key = request.headers.get("x-service-key") if service_key and SERVICE_KEY: if hmac.compare_digest(service_key, SERVICE_KEY): # Trusted — read user context from headers request.state.user_id = request.headers.get("x-user-id") request.state.tenant_id = request.headers.get("x-tenant-id") request.state.auth_type = "service_key" return # Path 2: Normal user auth (your existing flow, unchanged) authorization = request.headers.get("authorization", "") if authorization.startswith("Bearer "): token = authorization.removeprefix("Bearer ") claims = await verify_user_jwt(token) # your existing function request.state.user_id = claims.get("user_id") request.state.tenant_id = claims.get("tenant_id") request.state.auth_type = "user_jwt" return raise HTTPException(status_code=401, detail="Unauthorized")
Key detail: When the service key is present, your backend trusts the scoping headers (X-User-ID, X-Tenant-ID, etc.) because the caller has proven it’s a trusted service. Without the service key, those headers are ignored and normal user auth applies.
Always use constant-time comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node) to prevent timing attacks. Never use == to compare secrets.
Identity headers let your MCP server (and your backend) know who the end user is. Crow extracts claims from the user’s identity token and sends them as HTTP headers.
1. Your backend mints an identity JWT for the widget: { "user_id": "user_456", "email": "alice@example.com", "tenant_id": "t_789" }2. Widget sends it to Crow with every chat message3. Crow decodes the JWT and has the claims available4. For each header mapping you configured, Crow resolves the value: identity.user_id → "user_456" identity.email → "alice@example.com" identity.tenant_id → "t_789"5. Crow sends these as HTTP headers to your MCP server: X-User-ID: user_456 X-Email: alice@example.com X-Tenant-ID: t_7896. Your MCP server forwards them to your backend API
identity.* — Any claim from the user’s identity JWT:
Source
Value
identity.user_id
User ID (from user_id claim)
identity.email
Email (from email claim)
identity.name
Name (from name claim)
identity.<custom>
Any custom claim you include when minting the JWT
Identity claims are only available for authenticated users. If the user hasn’t been identified via Identity Verification, identity headers won’t be sent.
product.* — Fields from the Crow product configuration:
from fastmcp.dependencies import Depends, CurrentHeadersasync def get_user_id(headers: dict = CurrentHeaders()) -> str: """Read user ID forwarded by Crow.""" return headers.get("x-user-id", "")@mcp.tool()async def get_my_orders( user_id: str = Depends(get_user_id),) -> dict: """Get orders for the current user.""" if not user_id: return {"error": "User not authenticated"} return await db.orders.find_by_user(user_id)
Your backend middleware (see Adding Service Key Auth above).What happens when a user asks “show me my orders”:
Copy
Ask AI
1. Widget → Crow: identity_token (JWT with user_id: "user_456")2. Crow validates JWT, extracts user_id3. Crow → MCP server: X-Service-Key: HCF8n..., X-User-ID: user_4564. MCP server → Your backend: X-Service-Key: HCF8n..., X-User-ID: user_4565. Your backend validates service key ✓, reads X-User-ID6. Your backend queries orders for user_4567. Returns orders → MCP server → Crow → Widget → User sees their order list
Identity headers are not sent. Your MCP server receives the service key but no user context. Tools still work for non-user-specific actions.
Service key not set on product
X-Service-Key header is not sent. Your MCP server won’t be able to authenticate with your backend.
Identity claim doesn’t exist
If a header mapping references a claim not in the JWT (e.g., identity.tenant_id but you didn’t include tenant_id when minting), that header is simply not sent. No error.
MCP server is down
Crow surfaces an error to the user: “Could not connect to the MCP server.” The agent continues without those tools.