Foundations of Agent Authorization
written by Stefan Christoph
- 12 minutes readThe question that sounds new but isn’t
I spent a couple of weeks building a demo where a research agent paid for premium content on a user’s behalf. The agent had its own identity, but every purchase had to be tied back to a specific end user, with scoped credentials that let it act for that person and no one else. At one point the whole thing broke, and I lost time chasing what looked like an SDK bug. The real cause was mundane: a user ID mismatch, one identifier with a hyphen and one without. The credentials were scoped per user, so a lookup for the wrong user simply found nothing.
That bug was a good teacher. It forced me to answer a question out loud that a lot of agent projects handle by accident: who is this agent, and whose authority is it acting under right now?
If you have built anything with OAuth, you already know the answer. Agent authorization is not a new discipline. It is the same identity problem web applications solved years ago, wearing slightly different clothes. The goal of this post is to make that concrete: name the pieces, follow one token through a full chain, and then show exactly where the Model Context Protocol (MCP), the Agent2Agent protocol (A2A), and AWS services sit in the picture.
Two words people keep swapping: authentication and authorization
Every identity conversation starts here, and mixing them up is the root of most confusion.
Authentication answers who are you. Authorization answers what are you allowed to do. A passport proves who you are. A visa says which countries you may enter and for how long. You can hold a valid passport and still be refused entry, because the two checks are separate.
In OAuth and OIDC terms:
- OpenID Connect (OIDC) handles authentication. It issues an ID token that says “this is who the user is.”
- OAuth 2.0 handles authorization. It issues an access token that says “the bearer may do these specific things.”
OIDC is a thin layer on top of OAuth. OAuth was designed to grant access without handing over a password [4]; OIDC added a standard way to also learn identity [10]. Keep the two tokens separate in your head and most of the rest follows.
The cast: four roles
OAuth defines four roles, and every flow is just these four passing messages:
- Resource owner, the human (usually) who owns the data and can grant access to it.
- Client, the application that wants access. Your agent is a client. An MCP host is a client.
- Authorization server, issues tokens after checking identity and consent. Amazon Cognito can play this role.
- Resource server, holds the protected data or API and accepts valid access tokens.
Once you can point at each of these in a diagram, you can read any OAuth flow.
Confidential vs public clients
Not all clients are equally trustworthy, and OAuth treats them differently.
A public client cannot keep a secret. A single-page app or a mobile app ships its code to the user’s device, so any embedded secret is readable. A confidential client runs somewhere it can protect credentials, like a server.
Agents and MCP servers are almost always confidential clients: they run on infrastructure you control, so they can hold real credentials and authenticate themselves to the authorization server. They do that with a client secret, a signed assertion (private_key_jwt), or mutual TLS. This matters because a confidential client is trusted to request tokens directly, while a public client has to lean on extra protection, which is where PKCE comes in below.
The three shapes you actually need
Almost every agent auth scenario is one of three flows. Learn these three shapes and you can place any hop in an agent system.
Shape 1: a user delegates access (3LO)
Three-legged OAuth (3LO) is the flow you have clicked through a hundred times: “App X wants to access your Y. Allow?” Three parties are involved, which is where the name comes from: the user, the client, and the authorization server.
The mechanism is the Authorization Code flow with PKCE. The user consents, the authorization server hands back a short-lived code, and the client exchanges that code for an access token [4]. PKCE (Proof Key for Code Exchange) closes a gap where an attacker who intercepts the code could redeem it: the client commits to a secret up front and must prove it holds that secret when it swaps the code for a token [6]. OAuth 2.1, the draft that consolidates current best practice, makes PKCE the default for everyone [5].
Shape 1, user-delegated access (3LO), flowing top to bottom.
Use this when the agent acts for a specific person and that person is present to consent.
Shape 2: a service on its own behalf (2LO)
Sometimes there is no user. A backend job syncs data every night; nobody is sitting there to click “Allow.” This is two-legged OAuth (2LO), and it uses the Client Credentials flow: the client authenticates as itself and gets a token for the service itself, not for any person.
Shape 2, machine-to-machine (2LO), flowing top to bottom.
Use this for machine-to-machine work where the agent acts on its own behalf, not for a user.
Shape 3: acting on someone’s behalf, downstream (token exchange)
Here is the shape that trips people up, and the one agents hit constantly. An agent receives a token for itself, then needs to call a downstream API as the original user. It should not just forward the token it was handed. It needs a new token, scoped for the downstream service, that still carries the fact that a particular user authorized the action.
The standard mechanism is Token Exchange, RFC 8693. The client presents its current token as a subject_token and asks the authorization server for a fresh token whose audience is the downstream API. RFC 8693 distinguishes delegation (the new token records “the agent, acting for the user”) from impersonation (the new token looks like the user directly). Delegation is the honest, auditable default [7].
Shape 3, token exchange (RFC 8693), on behalf of the user, flowing top to bottom.
This is exactly the shape my payments demo needed: an agent buying content as a user, with a credential scoped to that user. When the user ID didn’t match, the scoped lookup returned nothing. The flow was correct; the identifier was wrong.
The tokens themselves
Three token types show up repeatedly, and knowing which is which prevents a lot of bugs:
- Access token, short-lived, presented to resource servers to authorize a call. This is the workhorse.
- ID token, from OIDC, describes who the user is. Meant for the client, not for calling APIs.
- Refresh token, longer-lived, used to obtain new access tokens without asking the user to log in again.
Two properties decide whether a token is accepted:
- Scopes say what the token may do (
drive.readonly). - Audience (
aud) says who the token is for. A resource server must reject a token whose audience is some other service. This single check blocks a whole class of attacks where a token issued for service A is replayed against service B.
Tokens come in two formats. A JWT is self-describing and can be validated locally by checking a signature. An opaque token is just a reference, and the resource server calls the authorization server’s introspection endpoint to ask “is this still valid, and what does it grant?” Both are fine; they trade local validation against central control.
Where MCP fits
Now the payoff. If you know the three shapes and the token taxonomy, MCP authorization reads like a checklist you already understand.
As of the 2025-06-18 authorization specification (the spec is versioned and evolving, so check modelcontextprotocol.io for the current revision), MCP makes a clear, deliberate choice: it reuses OAuth, it does not invent auth [1].
- A protected MCP server is an OAuth 2.1 resource server. The MCP host is the client.
- The authorization server is a separate entity (it may be co-hosted, but the roles are distinct). This is a notable change from early-2025 designs that folded the authorization server into the MCP server.
- Discovery is automatic. The client calls the MCP server, gets a
401 Unauthorizedwith aWWW-Authenticateheader, follows RFC 9728 Protected Resource Metadata to find the authorization server, then reads RFC 8414 Authorization Server Metadata to learn its endpoints [8]. - The client then runs Authorization Code with PKCE, and it must send an RFC 8707 resource indicator naming the exact MCP server the token is for. The server must validate that the token’s audience is itself [9].
- Critically, no token passthrough. If the MCP server needs to call an upstream API, it is an OAuth client to that API and obtains a separate upstream token. It never forwards the token it received from the host.
MCP authorization discovery and token flow.
Notice there is nothing exotic here. It is Shape 1 (auth code + PKCE) plus strict audience binding plus a discovery handshake. A precise note worth making: MCP does not mandate RFC 8693 token exchange. RFC 8693 is the general on-behalf-of primitive; MCP’s concrete rule is the narrower “obtain a separate upstream token, never pass through.” They point in the same direction, but they are not the same requirement.
Where A2A fits
The Agent2Agent protocol (v1.0.0) makes the same design choice for agent-to-agent calls: reuse standard web security, keep identity at the protocol layer [2].
Every A2A agent publishes an Agent Card, a JSON document describing its identity, skills, and, importantly, its securitySchemes. Those schemes are the familiar OpenAPI set: API key, HTTP auth (including Bearer), OAuth 2.0, OpenID Connect, and mutual TLS. The card is the machine-readable answer to “how do I authenticate to you?”
Credential acquisition happens out of band. A calling agent reads the card, obtains a credential through whatever scheme is declared (a client-credentials token for 2LO, or a user-delegated token for on-behalf-of work), and attaches it to every request. A2A does not define a new token type or a new handshake; it points at the ones that already exist.
A2A does add one nice touch for delegation. A task can enter a TASK_STATE_AUTH_REQUIRED state, which lets an agent say “I need authorization to continue” and hand that requirement back to its client. Because a client can itself be another agent, this can chain, forming a path of authorization requests back to the human who can actually consent. It is delegation made explicit in the task lifecycle.
If you’re running this on AWS
Everything above is vendor-neutral. Here is how it lands on AWS, and the mapping is direct.
Amazon Cognito plays the authorization server and OIDC provider. It issues the ID and access tokens, and it implements RFC 8707 resource indicators: name a resource server in the token request and Cognito stamps that server’s identifier into the aud claim [9]. That is the exact audience-binding MCP requires, which is why Cognito’s own docs call out resource indicators as important for MCP [3].
Amazon Bedrock AgentCore Identity handles the two directions an agent needs:
- Inbound auth authenticates the caller reaching your agent, validating a JWT bearer token (from Cognito or another OIDC provider) before the agent runs.
- Outbound auth vends the tokens your agent needs to call downstream services. It supports user-delegated access (3LO) and machine-to-machine access (2LO) through configured OAuth credential providers. In the SDK these are literally an
auth_flowofUSER_FEDERATIONorM2M.
The piece that ties it together is the managed token vault. AgentCore orchestrates the OAuth dance with the downstream provider, then stores the resulting access and refresh tokens keyed to the workload and the end user. Your agent code asks for a token with a decorator and gets one back; the vault handles storage, refresh, and re-consent. That is the on-behalf-of story from Shape 3, delivered as a managed capability rather than something you wire up by hand. For a foundations post the point is not the setup steps, it is the shape: a managed authorization server plus a managed vault remove the undifferentiated heavy lifting, so you spend your time on the agent, not on token plumbing.
The same flows on AWS: Amazon Cognito plus Bedrock AgentCore Identity.
The layer above OAuth
One honest scoping note. OAuth answers whose authority and what broad scopes. It does not, by itself, express fine-grained rules like “this agent may read invoices but only for its own department.” That is the job of an authorization model: role-based (RBAC), attribute-based (ABAC), or relationship-based (ReBAC), often implemented with a policy engine like Amazon Verified Permissions, Cedar, or OPA. Think of it as the next layer up. OAuth gets a correctly-scoped token to the right service; the policy layer decides what that token may touch once it arrives. Worth knowing it exists; it is a separate post.
The takeaway
The agent identity stack feels new because the actors are new: agents calling agents, agents calling tools, agents acting for users they may never see. But the machinery underneath is OAuth 2.0 and OIDC, the same flows that have signed you into apps for a decade. MCP wires them into a resource-server-plus-discovery pattern. A2A publishes them in an agent card. AgentCore Identity manages the tokens for you. None of it replaces what you know; all of it reuses it.
So the next time an agent design lands on your desk, don’t ask “what new security do we need?” Ask the older, better question my debugging session forced on me: who is this, and whose authority is it acting under? You already have the vocabulary to answer.
Which hop in your agent system is hardest to reason about right now: the user-delegated one, the machine-to-machine one, or the on-behalf-of downstream call?
Sources
- [1] MCP Authorization Specification (2025-06-18), MCP server as OAuth 2.1 resource server; RFC 9728 discovery, RFC 8414 metadata, RFC 8707 resource indicators, PKCE, no token passthrough.
- [2] A2A Protocol Specification (v1.0.0), Agent Card
securitySchemes, out-of-band credential acquisition,TASK_STATE_AUTH_REQUIRED. - [3] Amazon Bedrock AgentCore Identity, Obtain OAuth 2.0 access token, user-delegated (USER_FEDERATION / 3LO) vs M2M (2LO), managed token vault, refresh handling, Cognito + RFC 8707 resource indicators.
- [4] RFC 6749, The OAuth 2.0 Authorization Framework, roles, authorization code and client credentials grants.
- [5] draft-ietf-oauth-v2-1, OAuth 2.1 (IETF draft), PKCE by default, removal of implicit and password grants.
- [6] RFC 7636, Proof Key for Code Exchange (PKCE), protecting the authorization code flow.
- [7] RFC 8693, OAuth 2.0 Token Exchange, delegation vs impersonation, on-behalf-of tokens.
- [8] RFC 9728, OAuth 2.0 Protected Resource Metadata, resource-server metadata and
WWW-Authenticatediscovery. - [9] RFC 8707, Resource Indicators for OAuth 2.0, audience-binding tokens to a specific resource.
- [10] OpenID Connect Core 1.0, ID token and the identity layer over OAuth.
About the Author
Stefan Christoph is a Principal Solutions Architect at AWS, focused on agentic AI, media & entertainment, and helping builders move from demo to production. He writes about AI architecture, developer productivity, and the future of software.
This is a personal blog. Opinions expressed here are my own and do not represent the views or positions of my employer.
🎬 Also available as a blog walkthrough video on YouTube
❤️ Created with the support of AI (Kiro)
📝 Last updated: August 17, 2026 — Editorial polish for readability and voice