# Where the principal comes from

Canonical URL: https://vercel-mcp-reference.vercel.app/security/identity-and-principals/
Markdown: https://vercel-mcp-reference.vercel.app/security/identity-and-principals.md
Audience: security, engineer, architect. MCP spec version: 2026-07-28. Last reviewed: 2026-08-26. Status: stable.

**TL;DR:** every authorization decision in an MCP server keys off a principal, and there is exactly one safe source for it: the **verified access token**, established by the flow in [Authorization flows](https://vercel-mcp-reference.vercel.app/security/authorization/). [Tool](https://vercel-mcp-reference.vercel.app/glossary/#tool) arguments are generated by a model, and a model can be steered by anything it has read, so an identity read from arguments is an identity chosen by whoever last influenced the [prompt](https://vercel-mcp-reference.vercel.app/glossary/#prompt). On Vercel the plumbing is concrete: `verifyToken` returns an `AuthInfo`, `withMcpAuth` attaches it to the request, and your handler reads it from `ctx.http.authInfo`. Nothing the client or model sends in a payload should ever be able to change who a request acts as.

## The rule

A client or model must **never assert its own identity**. A server that trusts a client-supplied `principal`, `userId`, `email`, or `role` field is wide open: any caller can claim any principal by putting that string in the request. Nothing stops a low-privilege caller from sending `userId: "admin"` and inheriting that principal's grants.

This is a textbook privilege-escalation and confused-deputy failure: the server is tricked into acting with authority the caller does not hold (see [Trust boundaries](https://vercel-mcp-reference.vercel.app/patterns/trust-boundaries/)). It also breaks [least privilege](https://vercel-mcp-reference.vercel.app/patterns/least-privilege/) at the root: every scope check downstream is only as trustworthy as the principal it keys off, and a forgeable principal makes all of them meaningless. In an MCP system, tool arguments are LLM-generated and therefore untrusted input; identity is exactly the kind of value that must not be read from an untrusted payload. An attacker does not even need to compromise your client: a prompt-injection payload in a document the model summarized earlier is enough to steer the next tool call's arguments.

## The AuthInfo flow into handlers

Production derives the principal from the **verified token**, fixed at authentication time, and ignores any identity-shaped field in the arguments. On Vercel with `mcp-handler` the path is short and worth knowing end to end (verified against `mcp-handler` 2.1.1 and `@modelcontextprotocol/server` 2.0.0):

1. `withMcpAuth` extracts the bearer token and calls **your** `verifyToken(req, bearerToken)`.
2. `verifyToken` validates the token (signature, issuer, expiry, audience) and returns an `AuthInfo`: `{ token, clientId, scopes, expiresAt?, resource?, extra? }`. This return value is the trust decision; put the verified subject and any tenant or role claims you need into `extra`.
3. The wrapper attaches it to the request and `createMcpHandler` forwards it into the SDK, which delivers it to every tool handler as **`ctx.http.authInfo`** on the context object (the second callback argument).

```ts
server.registerTool(
  "read_invoice",
  { description: "Read one invoice", inputSchema: z.object({ id: z.string() }) },
  async ({ id }, ctx) => {
    const auth = ctx.http?.authInfo;            // set by withMcpAuth
    if (!auth) throw new Error("unauthenticated");
    requireScope(auth, "invoices:read");        // default deny
    const owner = auth.extra?.userId as string; // from verifyToken, not from args
    return readInvoiceFor(owner, id);
  },
);
```

The principal is fixed when the token is verified; every subsequent decision is attributed to that principal. If the arguments happen to contain a `userId`, the handler never reads it.

The two sourcing models side by side; the only difference is where the value the authorization check trusts comes from:

```mermaid
flowchart TB
    subgraph unsafe["Unsafe: identity read from the request"]
        direction LR
        cm1[Client / Model] -->|"principal in tool args (forgeable)"| h1[Handler] --> z1[Authorization]
    end
    subgraph safe["Safe: identity from the verified token"]
        direction LR
        vt["verifyToken: AuthInfo"] -->|"ctx.http.authInfo"| h2[Handler] --> z2[Authorization]
        cm2[Client / Model] -. "principal in tool args (ignored)" .-> h2
    end
```

Three rules keep the flow honest:

- **`verifyToken` decides, handlers consume.** Handlers never re-derive identity from headers or payloads; they read `ctx.http.authInfo` or refuse. A missing `authInfo` on a supposedly protected route means the gate was miswired (`required: false` is the default; see [Authorization flows](https://vercel-mcp-reference.vercel.app/security/authorization/)); fail closed.
- **Scopes are not the principal.** `requiredScopes` on `withMcpAuth` gates the route; per-principal decisions (which rows, which tenant, which tools are even listed) key off the verified claims inside `AuthInfo`. See [Authorization & scoping](https://vercel-mcp-reference.vercel.app/security/checklist/#authorization--scoping).
- **`AuthInfo.extra` carries claims, not conclusions.** Store the verified `sub` and tenant; compute "may this principal call this tool" fresh at call time, default deny.

## The teaching simplification in this repo

No example server in this repo takes the principal from a tool argument any more. The two servers that authorize per principal, `examples/secure-tools-server` (in the repository) (the house template) and `examples/least-privilege-server` (in the repository), both derive it from `ctx.http.authInfo`: the route wraps the handler in `withMcpAuth` with a stub `verifyToken` from `src/auth.ts`, and every handler calls `principalFromAuthInfo(ctx.http?.authInfo)` (the verified subject first, the OAuth `clientId` as fallback, and the empty string when no verified token reached the handler, which is never authorized, so a route that lost its auth wrapper degrades to denials, not to an open server). A `principal` argument is stripped by the schema and never consulted; the tests prove it can neither grant nor revoke access. `examples/auth-server` (in the repository) shows the same principal source with the RFC 9728 metadata route alongside.

The simplification that remains is in the **verifier**, and it is deliberate, for two reasons:

- **Clarity**: `verifyToken` is a fixed in-process table from bearer token to `AuthInfo` (in `secure-tools-server`, `demo-token` verifies to the authorized subject and `other-token` to a valid but different user; in `least-privilege-server`, `auditor-token`, `treasury-token`, and `stranger-token` verify to a read-only principal, a principal with both grants, and a correctly scoped user with no grants at all), so you can see exactly which claims the check keys off without tracing a JWKS fetch.
- **Offline testability**: the vitest suites inject `AuthInfo` through the in-memory transport's `send` options, the same path `withMcpAuth` populates in production, and drive both the allowed and the denied paths with no IdP and no network.

Only the **verifier** is stubbed; the principal source and the check itself (default deny, scope-set comparison inside every handler, per-principal listing in `least-privilege-server`) are production-shaped. A real deployment replaces the table with JWT verification against its authorization server (signature via JWKS, issuer, audience per RFC 8707, expiry) and puts the verified subject in `AuthInfo.extra.sub`; nothing else changes. Never ship the stub table, and never reintroduce an argument-sourced principal.

## Related

- [Authorization flows](https://vercel-mcp-reference.vercel.app/security/authorization/) - the OAuth 2.1 flow that produces the verified token this page keys off.
- [Security checklist](https://vercel-mcp-reference.vercel.app/security/checklist/) - the authorization and scoping checkboxes this rule underwrites.
- [Least privilege](https://vercel-mcp-reference.vercel.app/patterns/least-privilege/) - per-tool scope declarations that assume an unforgeable principal.
- [Trust boundaries](https://vercel-mcp-reference.vercel.app/patterns/trust-boundaries/) - the confused deputy, drawn at the architecture level.
- [Consent UX](https://vercel-mcp-reference.vercel.app/client-side/consent-ux/) - the human half of attribution: the user approving what runs as them.

## Bibliography

- Model Context Protocol Specification, *Authorization*, version 2026-07-28 - <https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization>
- Model Context Protocol, *Security Best Practices* (confused deputy, token passthrough) - <https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices>
- Vercel Documentation, *Deploy MCP servers to Vercel* - <https://vercel.com/docs/mcp/deploy-mcp-servers-to-vercel>
- Vercel, *mcp-handler* (source and API) - <https://github.com/vercel/mcp-handler>
- OWASP Top 10 for Large Language Model Applications - <https://owasp.org/www-project-top-10-for-large-language-model-applications/>
