{
  "question": "Can my MCP server treat an Mcp-Session-Id, or a cart or workflow ID it handed out, as proof of who the caller is?",
  "slug": "can-an-mcp-session-id-or-state-handle-be-used-to-authenticate-a-user",
  "url": "https://www.rightshold.com/answers/can-an-mcp-session-id-or-state-handle-be-used-to-authenticate-a-user",
  "short_answer": "No. The MCP security best practices say it in so many words: servers that implement authorization MUST verify all inbound requests, and MUST NOT use sessions for authentication (protocol 2025-11-25 and earlier) or treat possession of a state handle as authentication (2026-07-28). A session ID or handle is a lookup key, and anyone who obtains or guesses it can present it. The 2025-11-25 spec describes two attacks. In impersonation, an attacker reuses a session ID and the server answers as if they were the original user. In session-hijack prompt injection, on a server with several instances sharing an event queue, an attacker sends an event to one instance using someone else's session ID, and another instance delivers it to the real client as if it were a legitimate response, for example a tools/list_changed notification that makes the client pick up tools the user never enabled. The 2026-07-28 revision made MCP stateless, with no protocol-level sessions, so servers that need state now mint an explicit handle such as a cart or workflow ID and get it back as a tool argument; the same rule applies to those handles. The fix is the same in both: validate the access token on every request, derive the user from that token, generate handles with a secure random generator, and store state keyed as <user_id>:<handle> so a handle presented by anyone else finds nothing.",
  "checklist": [
    "Validate the bearer token on every request, including follow-up calls in the same conversation. Check signature or introspection, expiry, and that the audience is your MCP server. Never skip it because a request carries a known session ID or handle.",
    "Take the user's identity from the verified token, never from a session ID, handle, header or argument the client sends. The spec's example key format is <user_id>:<handle>, with the user ID derived from the token.",
    "Look up stored state with both parts. A handle that exists but belongs to another user should return the same not-found answer as a handle that doesn't exist, so it can't be used to probe which handles are live.",
    "Generate session IDs and handles with a cryptographically secure random generator (for example crypto.randomUUID() in Node or secrets.token_urlsafe() in Python). Don't use counters, timestamps, database row IDs or hashes of guessable values.",
    "Expire handles and session IDs, and rotate or end them when the user signs out or the token is revoked. Return 404 for an ended 2025-11-25 session so the client starts a new one.",
    "Keep session IDs and handles out of URLs, logs and analytics, and treat them as secrets in transit. A handle in a log is a handle anyone with log access can replay.",
    "If you run several server instances behind a shared queue or cache, key queued events by user and session together, and check on delivery that the event belongs to the user whose token opened the stream.",
    "Write a test that uses user A's token with user B's handle and expects not-found, and another that sends a known handle with no token and expects 401. Run both on every deploy."
  ],
  "sections": [
    {
      "heading": "What the MCP spec says about sessions and handles",
      "points": [
        "The 2025-11-25 Streamable HTTP transport lets a server assign a session ID in an MCP-Session-Id header on the InitializeResult response. The ID SHOULD be globally unique and cryptographically secure (a securely generated UUID, a JWT or a cryptographic hash), MUST contain only visible ASCII characters, and the client MUST send it on every later request. The server MAY end the session at any time and then answers that ID with 404.",
        "The 2026-07-28 revision describes MCP as a stateless protocol: everything needed to process a request is in the request, and no state is inferred from earlier requests on the same connection. Its security best practices replace the session hijacking section with state handle hijacking: a server mints a handle such as a shopping cart ID or workflow ID for an authenticated user, returns it in a tool result, and later receives it as an ordinary tool argument.",
        "In both versions the mitigation is the same: verify all inbound requests, don't treat the session ID or handle as authentication, use secure non-deterministic identifiers (expiring them also helps), and bind them server-side to the authenticated user."
      ]
    },
    {
      "heading": "The two attacks the spec describes",
      "points": [
        "Impersonation: a client authenticates and gets a persistent session ID. An attacker obtains or guesses it and calls the server with it. If the server doesn't check authorization again, it acts for the attacker as if they were the original user. The 2026-07-28 handle version is the same: the attacker calls a tool with someone else's cart or workflow ID and the server operates on that user's state.",
        "Prompt injection through a shared queue (2025-11-25): a client connects to server instance A and gets a session ID. An attacker sends a malicious event to instance B with that session ID; B puts it on a queue keyed only by session ID; A polls the queue and sends the payload to the real client as an asynchronous or resumed response. If the server sends tools/list_changed in response to a tool call, the spec notes the client could end up with tools it didn't know were enabled."
      ]
    },
    {
      "heading": "Why a random ID isn't enough on its own",
      "points": [
        "Random IDs stop guessing, not leaking. Session IDs and handles end up in client logs, proxy logs, error messages, screenshots, copied transcripts and model context, and a tool result containing a handle is visible to the model and anything that reads its output. Binding the handle to the user from the verified token is what makes a leaked handle useless to anyone else.",
        "Binding also closes the cross-tenant case: a legitimate user of your server who learns another customer's handle still can't use it, because their token maps to a different user ID."
      ]
    }
  ],
  "checks": [],
  "ask_rightshold": {
    "start": "https://www.rightshold.com/start?ref=seo",
    "mcp_endpoint": "https://www.rightshold.com/mcp",
    "ask": "Before my MCP server acts on a request from this agent, is the agent credential it presented valid, unexpired and unrevoked, for this exact configuration and this action?",
    "mcp_call": {
      "tool": "verify_agent_credential",
      "arguments": {
        "id": "crd_...",
        "config_hash": "<sha256 the agent presents>",
        "scope": "orders:place"
      }
    },
    "also": [
      "Free, keyless, milliseconds. It checks a Rightshold rs-credential/0.1 agent credential on each call: issuer signature, expiry, revocation, whether the configuration hash matches what was certified, and whether the requested scope is covered. It returns the owner's verification tier without disclosing who the owner is.",
      "It's an extra per-call check on which agent is calling, not a replacement for validating your own OAuth access tokens or binding your own handles to users. Rightshold doesn't see your sessions or your stored state."
    ],
    "scope": "Rightshold verifies agent credentials it issued and records actions agents route through it. It doesn't run your MCP server, validate your OAuth tokens, or store or check your session IDs and state handles."
  },
  "sources": [
    {
      "title": "MCP specification 2026-07-28: Security Best Practices (State Handle Hijacking)",
      "url": "https://modelcontextprotocol.io/specification/2026-07-28/basic/security_best_practices"
    },
    {
      "title": "MCP 2025-11-25: Security Best Practices (Session Hijacking)",
      "url": "https://modelcontextprotocol.io/docs/2025-11-25/tutorials/security/security_best_practices"
    },
    {
      "title": "MCP specification 2025-11-25: Transports (Streamable HTTP session management, MCP-Session-Id)",
      "url": "https://modelcontextprotocol.io/specification/2025-11-25/basic/transports"
    },
    {
      "title": "MCP specification 2026-07-28: Overview (Statelessness)",
      "url": "https://modelcontextprotocol.io/specification/2026-07-28/basic/index"
    },
    {
      "title": "OWASP Session Management Cheat Sheet",
      "url": "https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html"
    }
  ],
  "related": [
    {
      "title": "How to keep MCP as a thin adapter and recheck authorization on every tool call",
      "url": "https://www.rightshold.com/answers/how-to-keep-mcp-as-a-thin-adapter-and-recheck-authorization-on-every-tool-call"
    },
    {
      "title": "MCP OAuth: rejecting wrong-audience tokens",
      "url": "https://www.rightshold.com/answers/mcp-oauth-wrong-audience-tokens"
    },
    {
      "title": "How to share one MCP server across a company with per-user auth",
      "url": "https://www.rightshold.com/answers/how-to-share-one-mcp-server-across-a-company-with-per-user-auth"
    },
    {
      "title": "How to do OAuth token exchange so an MCP server never forwards user tokens",
      "url": "https://www.rightshold.com/answers/how-to-do-oauth-token-exchange-so-an-mcp-server-never-forwards-user-tokens"
    }
  ],
  "computed_from": "Curated guidance; license verdicts computed from Rightshold's license profiles at render time. Not legal advice.",
  "updated": "2026-11-10"
}