Four hops, four different credentials
- User → your appSSO session identifies the person
- App → Claude APIAPI key held server-side only
- Agent → MCP serveruser’s token, issued for this server
- Server → backendits own token — never forwarded
Authentication, authorization, and “on whose behalf?”
Authentication answers who is making a request; authorization answers what that party may do. In ordinary web apps the two are close together. In an agent system they drift apart, because the thing actually calling the database is neither the user nor your code — it is a tool invoked because a model decided to. So the architect’s core question at every hop is: on whose behalf is this call made, and does the system receiving it know that for certain?
Three facts shape the answer. First, the model can be influenced by the content it reads — Anthropic’s secure-deployment guide calls this prompt injection and recommends defence in depth rather than trusting the model to refuse. Second, a tool’s arguments are written by the model, so anything security-relevant in them (a customer ID, an account number, a file path) is untrusted input. Third, whatever credential a tool runs with is the ceiling of what an injected instruction can achieve. Authorization therefore has to be enforced by systems outside the model, using identity the model cannot forge.
| Hop | Who must be identified | Credential | Typical gap |
|---|---|---|---|
| User → application | The person | SSO / session | Agent endpoint left unauthenticated for a demo |
| Application → Claude API | Your organisation | API key | Key shipped in a browser or mobile app |
| Agent → MCP server or tool | The user the agent acts for | OAuth access token for that server | One shared service account for every user |
| Tool server → backend API | The tool server | The server’s own upstream token | Forwarding the token it received (token passthrough) |
How MCP does it: OAuth 2.1, bound to one server
For remote MCP servers over HTTP, the MCP specification builds authorization on OAuth 2.1. The MCP server is an OAuth resource server; the MCP client (Claude Code, a Claude app, or your own agent) is an OAuth client acting for a user; a separate authorization server signs the user in and issues tokens. Authorization is optional in MCP, and for local servers on the STDIO transport the specification says not to use this flow but to take credentials from the environment instead.
An MCP client connecting to a protected server
resource_metadata URLresourceAuthorization: Bearerresource parameter on the way in, and an audience check on the server before it does anything.The specification’s hard requirements are the checklist for any review. Clients must use PKCE, must send the resource parameter naming the MCP server in both the authorization and token requests, and must send the token in the Authorization header — never in the URL query string. Servers must publish Protected Resource Metadata so clients can find the authorization server, must validate that each token was issued for them, must answer invalid or expired tokens with 401, and must not accept or pass on tokens issued for anything else. Missing permissions get a 403 with error="insufficient_scope" and the scopes needed, which lets a client step up to wider access only when an operation needs it.
The gaps that recur in agent designs
Most review findings fall into a handful of patterns. Use them as a checklist against any architecture diagram you are given.
Reviewing a hospital’s appointment assistant
- Passes: Patients sign in via the portal’s SSOthe person is authenticated
- Fails: Tools call the scheduling API as one service accountevery patient gets every patient’s access
- Fails:
get_appointmentstakespatient_idfrom the modelan injected ID reaches other records - Check: System prompt says “only discuss the user’s own bookings”useful, but not a control
- Fails: Service account holds
records:*scopefar wider than scheduling needs - Missing: Per-user audit trail of tool callscannot show who saw what
| Gap | Why it matters | Close it by |
|---|---|---|
| Shared service account behind the agent | The agent can do anything any user could; users inherit each other’s reach | Delegated, per-user tokens; the backend applies that user’s permissions |
| Identity taken from tool arguments | An injected instruction can name another user or account | Bind identity from the authenticated session on the server side |
| Token passthrough | Downstream checks bypassed; audit trail wrong; replay across services | The tool server obtains its own upstream token |
Omnibus scopes (*, full-access) | A leaked token opens everything; revocation breaks every workflow | Minimal starting scopes; step up with insufficient_scope challenges |
| Secrets inside the agent’s environment | Anything the agent can read, an injection can try to exfiltrate | Inject credentials at a proxy outside the agent’s boundary |
| Prompt text as the access policy | Probabilistic, and overridable by content the model reads | Enforce in code and permissions; keep the prompt as a second layer |
| Session ID treated as proof of identity | A guessed or stolen ID impersonates the user | Verify every request; bind sessions to the user ID from the token |
def get_appointments(tool_input: dict, session: Session) -> dict:
# tool_input is written by the model: treat it as untrusted.
patient = session.patient # set by SSO middleware, not by Claude
if "appointments:read" not in session.scopes:
return {"is_error": True, "content": "Missing scope appointments:read"}
appts = scheduling_api.list(
patient_id=patient.id, # ignore any patient_id Claude supplies
token=session.scheduling_token, # this user's token, for this API only
after=tool_input.get("after"),
)
return {"content": [{"date": a.date, "clinic": a.clinic} for a in appts]}Choosing the right credential pattern
Which credential pattern fits this call?
- A signed-in userDelegated OAuth tokenper user, audience-bound
- No user — a scheduled jobService identitynarrow scopes, own audit trail
- A local STDIO toolCredentials from envsandboxed, least privilege
- Agent must not see secretProxy injects itoutside the agent’s boundary
The proxy pattern deserves a closer look because it closes a gap the others do not. Anthropic’s secure-deployment guide recommends running a proxy outside the agent’s security boundary that adds credentials to outgoing requests: the agent never sees the key, the proxy can enforce an allowlist of destinations, and every request is logged in one place. It also warns that even a read-only mount of a code directory can expose .env files, cloud credentials and private keys, so those should be excluded before the agent sees the directory.
Where the API does the MCP call for you, the responsibilities move. With the Claude API’s MCP connector, your application runs the OAuth flow, obtains and refreshes the token, and passes it in the authorization_token field; Anthropic’s servers then call the MCP server. The documentation also notes the connector is not covered by zero-data-retention arrangements — a data-handling question to raise in the same review (compliance itself is 5.4).
Closing gaps in Claude Code and the Agent SDK
When the agent is Claude Code or built on the Agent SDK, the same thinking applies to its own permissions. Claude Code evaluates permission rules in a fixed order — deny, then ask, then allow — and the first match wins, so an allow rule cannot carve an exception out of a deny rule. Organisations can put rules in managed settings that users cannot override, and can disable the bypass-permissions mode there. Remote MCP servers authenticate through OAuth with /mcp, and project-scoped servers from a checked-in .mcp.json need approval before first use.
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./secrets/**)",
"Bash(curl *)",
"mcp__prod-db"
],
"disableBypassPermissionsMode": "disable"
}
}Traps the wrong answers are built from
| Tempting but wrong | Do this instead |
|---|---|
| Relying on the system prompt to keep users to their own data | Enforce access in the tool server or backend using the authenticated user’s identity. |
| One shared service account behind an agent that serves many users | Use delegated per-user OAuth tokens so existing permissions apply. |
Taking customer_id or user_id from the model’s tool arguments | Bind identity from the session server-side; treat tool input as untrusted. |
| An MCP server forwarding the client’s token to a downstream API | Validate the token’s audience, then call downstream with the server’s own credential. |
| Requesting every scope up front “to avoid prompts later” | Start with minimal scopes and step up on an insufficient_scope challenge. |
You should now be able to
- Trace identity and credentials across each hop of a Claude integration and name the gap at each.
- Explain the MCP authorization model: resource server, client, authorization server, PKCE and audience-bound tokens.
- Identify token passthrough, confused-deputy and session-hijack risks in an MCP server design.
- Choose between delegated user tokens, service identities, environment credentials and proxy injection.
- Recognise when a control is only in the prompt and move it into code, permissions or infrastructure.
- Apply Claude Code permission precedence and managed settings to close gaps for a team.